Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion frontend/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
cmake_minimum_required(VERSION 3.26)

project(catalyst_frontend LANGUAGES CXX)
project(catalyst_frontend)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

##############
## nanobind ##
##############

# nanobind suggests including these lines to configure CMake to perform an optimized release build
# by default unless another build type is specified. Without this addition, binding code may run
# slowly and produce large binaries.
# See https://nanobind.readthedocs.io/en/latest/building.html#preliminaries
if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()

find_package(Python REQUIRED
COMPONENTS Interpreter Development.Module NumPy
OPTIONAL_COMPONENTS Development.SABIModule
)

execute_process(
COMMAND "${Python_EXECUTABLE}" -c "import nanobind; print(nanobind.cmake_dir())"
OUTPUT_VARIABLE nanobind_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
)

find_package(nanobind CONFIG REQUIRED)

#####################
## catalyst subdir ##
#####################

add_subdirectory(catalyst)
31 changes: 31 additions & 0 deletions frontend/catalyst/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
#######################################
## default_pipelines nanobind module ##
#######################################

set(DEFAULT_PIPELINES_SRC_FILES
default_pipelines.cpp
)

# Target the stable ABI for Python 3.12+,
# which reduces the number of binary wheels that must be built
# (`STABLE_ABI` does nothing on older Python versions).
nanobind_add_module(default_pipelines STABLE_ABI
${DEFAULT_PIPELINES_SRC_FILES}
)

# This is necessary for compatibility with setuptools build extensions
set_target_properties(default_pipelines PROPERTIES
SUFFIX ".so"
)

set(CATALYST_ROOT_DIR "${PROJECT_SOURCE_DIR}/..")
set(CATALYST_MLIR_INCLUDE_DIR "${CATALYST_ROOT_DIR}/mlir/include")

target_include_directories(default_pipelines PRIVATE
${CATALYST_MLIR_INCLUDE_DIR}
)

###################
### utils subdir ##
###################

add_subdirectory(utils)
38 changes: 38 additions & 0 deletions frontend/catalyst/default_pipelines.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2025 Xanadu Quantum Technologies Inc.

// 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.

#include <nanobind/nanobind.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>

#include "Driver/DefaultPipelines.h"

namespace nb = nanobind;

NB_MODULE(default_pipelines, m)
{
m.doc() = "Bindings for Catalyst default pipelines.";
m.def("get_pipeline_names", &catalyst::driver::getPipelineNames,
"Returns the list of pipeline names.");
m.def("get_quantum_compilation_stage", &catalyst::driver::getQuantumCompilationStage,
"Returns the list of pass names for the Quantum Compilation stage.");
m.def("get_hlo_lowering_stage", &catalyst::driver::getHLOLoweringStage,
"Returns the list of pass names for the HLO Lowering stage.");
m.def("get_gradient_lowering_stage", &catalyst::driver::getGradientLoweringStage,
"Returns the list of pass names for the Gradient Lowering stage.");
m.def("get_bufferization_stage", &catalyst::driver::getBufferizationStage,
"Returns the list of pass names for the Bufferization stage.");
m.def("get_convert_to_llvm_stage", &catalyst::driver::getLLVMDialectLoweringStage,
"Returns the list of pass names for the LLVM Dialect Lowering stage.");
}
177 changes: 12 additions & 165 deletions frontend/catalyst/pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@
import sys
from copy import deepcopy
from dataclasses import dataclass
from functools import partial
from io import TextIOWrapper
from operator import is_not
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union

from catalyst.default_pipelines import (
get_bufferization_stage,
get_convert_to_llvm_stage,
get_gradient_lowering_stage,
get_hlo_lowering_stage,
get_quantum_compilation_stage,
)
from catalyst.utils.exceptions import CompileError

PipelineStage = Tuple[str, List[str]]
Expand Down Expand Up @@ -200,172 +205,14 @@ def get_stages(self) -> PipelineStages:
"""Returns all stages in order for compilation"""
# Dictionaries in python are ordered
stages = {}
stages["QuantumCompilationStage"] = get_quantum_compilation_stage(self)
stages["HLOLoweringStage"] = get_hlo_lowering_stage(self)
stages["GradientLoweringStage"] = get_gradient_lowering_stage(self)
stages["BufferizationStage"] = get_bufferization_stage(self)
stages["MLIRToLLVMDialectConversion"] = get_convert_to_llvm_stage(self)
stages["QuantumCompilationStage"] = get_quantum_compilation_stage(self.disable_assertions)
stages["HLOLoweringStage"] = get_hlo_lowering_stage()
stages["GradientLoweringStage"] = get_gradient_lowering_stage()
stages["BufferizationStage"] = get_bufferization_stage(self.async_qnodes)
stages["MLIRToLLVMDialectConversion"] = get_convert_to_llvm_stage(self.async_qnodes)
return list(stages.items())


def get_quantum_compilation_stage(_options: CompileOptions) -> List[str]:
"""Returns the list of passes that performs quantum compilation"""

user_transform_passes = [
# We want the invariant that transforms that generate multiple
# tapes will generate multiple qnodes. One for each tape.
# Split multiple tapes enforces that invariant.
"split-multiple-tapes",
# Run the transform sequence defined in the MLIR module
"builtin.module(apply-transform-sequence)",
# Nested modules are something that will be used in the future
# for making device specific transformations.
# Since at the moment, nothing in the runtime is using them
# and there is no lowering for them,
# we inline them to preserve the semantics. We may choose to
# keep inlining modules targeting the Catalyst runtime.
# But qnodes targeting other backends may choose to lower
# this into something else.
"inline-nested-module",
"lower-mitigation",
"adjoint-lowering",
"disable-assertion" if _options.disable_assertions else None,
]
return list(filter(partial(is_not, None), user_transform_passes))


def get_hlo_lowering_stage(_options: CompileOptions) -> List[str]:
"""Returns the list of passes to lower StableHLO to upstream MLIR dialects."""
hlo_lowering = [
"canonicalize",
"func.func(chlo-legalize-to-stablehlo)",
"func.func(stablehlo-legalize-control-flow)",
"func.func(stablehlo-aggressive-simplification)",
"stablehlo-legalize-to-linalg",
"func.func(stablehlo-legalize-to-std)",
"func.func(stablehlo-legalize-sort)",
"stablehlo-convert-to-signless",
"canonicalize",
"scatter-lowering",
"hlo-custom-call-lowering",
"cse",
"func.func(linalg-detensorize{aggressive-mode})",
"detensorize-scf",
"detensorize-function-boundary",
"canonicalize",
"symbol-dce",
]
return hlo_lowering


def get_gradient_lowering_stage(_options: CompileOptions) -> List[str]:
"""Returns the list of passes that performs gradient lowering"""

gradient_lowering = [
"annotate-invalid-gradient-functions",
"lower-gradients",
]
return gradient_lowering


def get_bufferization_stage(options: CompileOptions) -> List[str]:
"""Returns the list of passes that performs bufferization"""

bufferization_options = """bufferize-function-boundaries
allow-return-allocs-from-loops
function-boundary-type-conversion=identity-layout-map
unknown-type-conversion=identity-layout-map""".replace(
"\n", " "
)
if options.async_qnodes:
bufferization_options += " copy-before-write"

bufferization = [
"inline",
"convert-tensor-to-linalg", # tensor.pad
"convert-elementwise-to-linalg", # Must be run before --one-shot-bufferize
"gradient-preprocess",
# "eliminate-empty-tensors",
# Keep eliminate-empty-tensors commented out until benchmarks use more structure
# and produce functions of reasonable size. Otherwise, eliminate-empty-tensors
# will consume a significant amount of compile time along with one-shot-bufferize.
####################
"one-shot-bufferize{" + bufferization_options + "}",
####################
"canonicalize", # Remove dead memrefToTensorOp's
"gradient-postprocess",
# introduced during gradient-bufferize of callbacks
"func.func(buffer-hoisting)",
"func.func(buffer-loop-hoisting)",
# TODO: investigate re-adding this after new buffer dealloc pipeline
# removed due to high stack memory use in nested structures
# "func.func(promote-buffers-to-stack)",
# TODO: migrate to new buffer deallocation "buffer-deallocation-pipeline"
"func.func(buffer-deallocation)",
"convert-arraylist-to-memref",
"convert-bufferization-to-memref",
"canonicalize", # Must be after convert-bufferization-to-memref
# otherwise there are issues in lowering of dynamic tensors.
# "cse",
"cp-global-memref",
]

return bufferization


def get_convert_to_llvm_stage(options: CompileOptions) -> List[str]:
"""Returns the list of passes that lowers MLIR upstream dialects to LLVM Dialect"""

convert_to_llvm = [
"qnode-to-async-lowering" if options.async_qnodes else None,
"async-func-to-async-runtime" if options.async_qnodes else None,
"async-to-async-runtime" if options.async_qnodes else None,
"convert-async-to-llvm" if options.async_qnodes else None,
"expand-realloc",
"convert-gradient-to-llvm",
"memrefcpy-to-linalgcpy",
"func.func(convert-linalg-to-loops)",
"convert-scf-to-cf",
# This pass expands memref ops that modify the metadata of a memref (sizes, offsets,
# strides) into a sequence of easier to analyze constructs. In particular, this pass
# transforms ops into explicit sequence of operations that model the effect of this
# operation on the different metadata. This pass uses affine constructs to materialize
# these effects. Concretely, expanded-strided-metadata is used to decompose
# memref.subview as it has no lowering in -finalize-memref-to-llvm.
"expand-strided-metadata",
"lower-affine",
"arith-expand", # some arith ops (ceildivsi) require expansion to be lowered to llvm
"convert-complex-to-standard", # added for complex.exp lowering
"convert-complex-to-llvm",
"convert-math-to-llvm",
# Run after -convert-math-to-llvm as it marks math::powf illegal without converting it.
"convert-math-to-libm",
"convert-arith-to-llvm",
"memref-to-llvm-tbaa", # load and store are converted to llvm with tbaa tags
"finalize-memref-to-llvm{use-generic-functions}",
"convert-index-to-llvm",
"convert-catalyst-to-llvm",
"convert-quantum-to-llvm",
# There should be no identical code folding
# (`mergeIdenticalBlocks` in the MLIR source code)
# between convert-async-to-llvm and
# add-exception-handling.
# So, if there's a pass from the beginning
# of this list to here that does folding
# add-exception-handling will fail to add async.drop_ref
# correctly. See https://github.com/PennyLaneAI/catalyst/pull/995
"add-exception-handling" if options.async_qnodes else None,
"emit-catalyst-py-interface",
# Remove any dead casts as the final pass expects to remove all existing casts,
# but only those that form a loop back to the original type.
"canonicalize",
"reconcile-unrealized-casts",
"gep-inbounds",
"register-inactive-callback",
]
return list(filter(partial(is_not, None), convert_to_llvm))


def default_pipeline() -> PipelineStages:
"""Return the pipeline stages for default Catalyst workloads.

Expand Down
29 changes: 3 additions & 26 deletions frontend/catalyst/utils/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,29 +1,6 @@
cmake_minimum_required(VERSION 3.26)

project(catalyst_frontend)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# nanobind suggests including these lines to configure CMake to perform an optimized release build
# by default unless another build type is specified. Without this addition, binding code may run
# slowly and produce large binaries.
# See https://nanobind.readthedocs.io/en/latest/building.html#preliminaries
if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()

# Locate Python & nanobind
find_package(Python REQUIRED
COMPONENTS Interpreter Development.Module NumPy
OPTIONAL_COMPONENTS Development.SABIModule
)
execute_process(
COMMAND "${Python_EXECUTABLE}" -c "import nanobind; print(nanobind.cmake_dir())"
OUTPUT_VARIABLE nanobind_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
)
find_package(nanobind CONFIG REQUIRED)
#############################
## wrapper nanobind module ##
#############################

# Source file list for `wrapper` module
set(WRAPPER_SRC_FILES
Expand Down
Loading