# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.
# ruff: noqa: E402, E501, F401

"""
.. _customize_opt:

Customize Optimization
======================
One main design goal of Apache TVM is to enable easy customization of the optimization pipeline
for both research or development purposes and iterate the engineering optimizations. In this
tutorial we will

.. contents:: Table of Contents
    :local:
    :depth: 1
"""

######################################################################
# Review Overall Flow
# -------------------
# .. figure:: https://raw.githubusercontent.com/tlc-pack/web-data/main/images/design/tvm_overall_flow.svg
#    :align: center
#    :width: 80%
#
# The overall flow consists of the following steps:
#
# - **Construct or Import a Model**: Construct a neural network model or import a pre-trained
#   model from other frameworks (e.g. PyTorch, ONNX), and create the TVM IRModule, which contains
#   all the information needed for compilation, including high-level Relax functions for
#   computational graph, and low-level TensorIR functions for tensor program.
# - **Perform Composable Optimizations**: Perform a series of optimization transformations,
#   such as graph optimizations, tensor program optimizations, and library dispatching.
# - **Build and Universal Deployment**: Build the optimized model to a deployable module to the
#   universal runtime, and execute it on different devices, such as CPU, GPU, or other accelerators.
#

import os
import tempfile

import numpy as np

import tvm
from tvm import IRModule, relax
from tvm.relax.frontend import nn

######################################################################
# Composable IRModule Optimization
# --------------------------------
# Apache TVM provides a flexible way to optimize the IRModule. Everything centered
# around IRModule optimization can be composed with existing pipelines. Note that each optimization
# can focus on **part of the computation graph**, enabling partial lowering or partial optimization.
#
# In this tutorial, we will demonstrate how to optimize a model with Apache TVM.

######################################################################
# Prepare a Relax Module
# ~~~~~~~~~~~~~~~~~~~~~~~~~~
# We first prepare a Relax module. The module can be imported from other frameworks, constructed
# with NN module frontend or TVMScript. Here we use a simple neural network model as an example.


class RelaxModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.relu1 = nn.ReLU()
        self.fc2 = nn.Linear(256, 10, bias=False)

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu1(x)
        x = self.fc2(x)
        return x


input_shape = (1, 784)
mod, params = RelaxModel().export_tvm({"forward": {"x": nn.spec.Tensor(input_shape, "float32")}})
mod.show()

######################################################################
# Library Dispatch
# ~~~~~~~~~~~~~~~~
# We would like to quickly try out a variant of library optimization for certain platforms
# (e.g., GPU). We can write a certain dispatching pass for the specific platform and
# operator. Here we demonstrate how to dispatch the CUBLAS library for certain patterns.
#
# .. note::
#   This tutorial only demonstrates a single operator dispatching for CUBLAS, highlighting
#   the flexibility of the optimization pipeline. In real-world cases, we can import multiple
#   patterns and dispatch them to different kernels.


# Import cublas pattern
try:
    import tvm.relax.backend.cuda.cublas as _cublas
except ImportError as e:
    raise ImportError(
        "This tutorial requires TVM built with CUDA support.\n"
        "If you hit missing 'tvm_ffi', try: pip install apache-tvm-ffi\n"
        "Otherwise build TVM with CUDA enabled:\n"
        "  https://tvm.apache.org/docs/install/from_source.html\n"
        f"Original error: {e}"
    ) from e


# Define a new pass for CUBLAS dispatch
@tvm.transform.module_pass(opt_level=0, name="CublasDispatch")
class CublasDispatch:
    def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
        # Check if CUBLAS is enabled
        if not tvm.get_global_func("relax.ext.cublas", True):
            raise Exception("CUBLAS is not enabled.")

        # Get interested patterns
        patterns = [relax.backend.get_pattern("cublas.matmul_transposed_bias_relu")]
        # Note in real-world cases, we usually get all patterns
        # patterns = relax.backend.get_patterns_with_prefix("cublas")

        # Fuse ops by patterns and then run codegen
        mod = relax.transform.FuseOpsByPattern(patterns, annotate_codegen=True)(mod)
        mod = relax.transform.RunCodegen()(mod)
        return mod


mod = CublasDispatch()(mod)
mod.show()

######################################################################
# After the dispatching pass, we can see that the first ``nn.Linear`` and ``nn.ReLU`` are fused
# and rewritten to a ``call_dps_packed`` function which call the CUBLAS library. Notably, the
# other part is not changed, which means we can selectively dispatch the optimization for
# certain computation.

######################################################################
# Auto Tuning
# ~~~~~~~~~~~
# Continuing from the previous example, we can further optimize the model with auto-tuning for
# the **rest part of the computation**. Here we demonstrate how to use the meta-schedule to auto-tune
# the model.
#
# We can use ``MetaScheduleTuneTIR`` pass to simply tuning the model, while ``MetaScheduleApplyDatabase``
# pass to apply the best configuration to the model. The tuning process will generate search space,
# tune the model and the following steps will apply the best configuration to the model. Before
# running the passes, we need to lowering relax operator into TensorIR functions via ``LegalizeOps``
#
# .. note::
#
#   To save CI time and avoid flakiness, we skip the tuning process in CI environment.
#

device = tvm.cuda(0)
target = tvm.target.Target.from_device(device)
if os.getenv("CI", "") != "true":
    trials = 2000
    with target, tempfile.TemporaryDirectory() as tmp_dir:
        mod = tvm.ir.transform.Sequential(
            [
                relax.get_pipeline("zero"),
                relax.transform.MetaScheduleTuneTIR(work_dir=tmp_dir, max_trials_global=trials),
                relax.transform.MetaScheduleApplyDatabase(work_dir=tmp_dir),
            ]
        )(mod)

    mod.show()

######################################################################
# DLight Rules
# ~~~~~~~~~~~~
# DLight rules are a set of default rules for scheduling and optimization the kernel.
# DLight rules are designed for fast compilation and **fair** performance. In some cases,
# e.g. language model, DLight provides excellent performance, while for generic models,
# it achieves a balance between performance and compilation time.

from tvm.s_tir import dlight as dl

# Apply DLight rules
with target:
    mod = tvm.ir.transform.Sequential(
        [
            relax.get_pipeline("zero"),
            dl.ApplyDefaultSchedule(  # pylint: disable=not-callable
                dl.gpu.Matmul(),
                dl.gpu.GEMV(),
                dl.gpu.Reduction(),
                dl.gpu.GeneralReduction(),
                dl.gpu.Fallback(),
            ),
        ]
    )(mod)

mod.show()

######################################################################
# .. note::
#
#   This tutorial focuses on the demonstration of the optimization pipeline, instead of
#   pushing the performance to the limit. The current optimization may not be the best.


######################################################################
# Deploy the Optimized Model
# --------------------------
# We can build and deploy the optimized model to the TVM runtime.

ex = tvm.compile(mod, target="cuda")
dev = tvm.device("cuda", 0)
vm = relax.VirtualMachine(ex, dev)
# Need to allocate data and params on GPU device
data = tvm.runtime.tensor(np.random.rand(*input_shape).astype("float32"), dev)
gpu_params = [tvm.runtime.tensor(np.random.rand(*p.shape).astype(p.dtype), dev) for _, p in params]
gpu_out = vm["forward"](data, *gpu_params).numpy()
print(gpu_out)


######################################################################
# Summary
# -------
# This tutorial demonstrates how to customize the optimization pipeline for ML models in Apache TVM.
# We can easily compose the optimization passes and customize the optimization for different parts
# of the computation graph. The flexibility of the optimization pipeline enables us to quickly
# iterate the optimization and improve the performance of the model.
#
