# 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.
# pylint: disable=missing-docstring
# ruff: noqa: E501, E741, F841
import tvm.testing
from tvm.ir import assert_structural_equal
from tvm.s_tir import dlight as dl
from tvm.script import ir as I
from tvm.script import tirx as T
from tvm.target import Target


def test_fallback():
    @I.ir_module(s_tir=True)
    class Before:
        @T.prim_func(s_tir=True)
        def main(
            A: T.Buffer((1, 32, 1, 128), "float16"),
            C: T.Buffer((1, 1, 4096), "float16"),
        ):
            B = T.sblock_alloc_buffer((1, 1, 32, 128), "float16")
            for i, j, k, l in T.grid(1, 1, 32, 128):
                with T.sblock("T_transpose"):
                    vi, vj, vk, vl = T.axis.remap("SSSS", [i, j, k, l])
                    B[vi, vj, vk, vl] = A[vi, vk, vj, vl]
            for i, j, k in T.grid(1, 1, 4096):
                with T.sblock("T_reshape"):
                    vi, vj, vk = T.axis.remap("SSS", [i, j, k])
                    C[vi, vj, vk] = B[0, 0, vk % 4096 // 128, vk % 128]

    @I.ir_module(s_tir=True)
    class After:
        @T.prim_func(s_tir=True)
        def main(
            A: T.Buffer((1, 32, 1, 128), "float16"),
            C: T.Buffer((1, 1, 4096), "float16"),
        ):
            T.func_attr({"tirx.is_scheduled": True})
            for ax0_fused_0 in T.thread_binding(4, thread="blockIdx.x"):
                for ax0_fused_1 in T.thread_binding(1024, thread="threadIdx.x"):
                    with T.sblock("T_reshape"):
                        v0 = T.axis.spatial(4096, ax0_fused_0 * 1024 + ax0_fused_1)
                        T.reads(A[0, v0 // 128, 0, v0 % 128])
                        T.writes(C[0, 0, v0])
                        C[0, 0, v0] = A[0, v0 // 128, 0, v0 % 128]

    target = Target("nvidia/geforce-rtx-3090-ti")
    with target:
        mod = dl.ApplyDefaultSchedule(  # pylint: disable=not-callable
            dl.gpu.Fallback(),
        )(Before)
    assert_structural_equal(mod, After)


def test_fallback_reduction():
    @I.ir_module(s_tir=True)
    class Module:
        @T.prim_func(s_tir=True)
        def main(A: T.Buffer((1, 6144), "float32"), B: T.Buffer((1,), "float32")):
            for ax0, ax1 in T.grid(1, 6144):
                with T.sblock("block"):
                    v0 = T.axis.spatial(1, ax0)
                    v1 = T.axis.reduce(6144, ax1)
                    T.reads(A[v0, v1])
                    T.writes(B[v0])
                    with T.init():
                        B[v0] = T.float32(0)
                    B[v0] = B[v0] + T.Cast("float32", A[v0, v1])

    @I.ir_module(s_tir=True)
    class Expected:
        @T.prim_func(s_tir=True)
        def main(A: T.Buffer((1, 6144), "float32"), B: T.Buffer((1,), "float32")):
            T.func_attr({"tirx.is_scheduled": True})
            for ax0_fused_0 in T.thread_binding(T.int64(1), thread="blockIdx.x"):
                for ax0_fused_1 in T.thread_binding(T.int64(1024), thread="threadIdx.x"):
                    with T.sblock("block_init"):
                        v0 = T.axis.spatial(T.int64(1), T.int64(0))
                        T.where(ax0_fused_0 * T.int64(1024) + ax0_fused_1 < T.int64(1))
                        T.reads()
                        T.writes(B[0])
                        B[0] = T.float32(0)
                    for ax1 in range(6144):
                        with T.sblock("block_update"):
                            v0 = T.axis.spatial(T.int64(1), T.int64(0))
                            v1 = T.axis.reduce(6144, ax1)
                            T.where(ax0_fused_0 * T.int64(1024) + ax0_fused_1 < T.int64(1))
                            T.reads(B[0], A[0, v1])
                            T.writes(B[0])
                            B[0] = B[0] + T.Cast("float32", A[0, v1])

    with Target("apple/m1-gpu"):
        mod = dl.ApplyDefaultSchedule(  # pylint: disable=not-callable
            dl.gpu.Fallback(),
        )(Module)
    assert_structural_equal(mod, Expected)


def test_fallback_irregular_spatial():
    @T.prim_func(private=True, s_tir=True)
    def func(
        var_pages: T.handle,
        var_page_table_indptr: T.handle,
        var_page_table_values: T.handle,
        var_values: T.handle,
        seq_id: T.int32,
    ):
        nhead = T.int32()
        nlayer = T.int32()
        seqlen = T.int32()
        npage = T.int32()
        page_size = T.int32()
        num_total_pages = T.int32()
        num_total_seqs_plus_1 = T.int32()

        pages = T.match_buffer(var_pages, (num_total_pages, nlayer, nhead, page_size), "float16")
        page_table_indptr = T.match_buffer(var_page_table_indptr, (num_total_seqs_plus_1,), "int32")
        page_table_values = T.match_buffer(var_page_table_values, (npage,), "int32")
        values = T.match_buffer(var_values, (nlayer, nhead, seqlen), "float16")

        for l, h, pos in T.grid(nlayer, nhead, seqlen):
            with T.sblock("block"):
                vl, vh, vp = T.axis.remap("SSS", [l, h, pos])
                values[vl, vh, vp] = pages[
                    page_table_values[page_table_indptr[seq_id] + T.floordiv(vp, page_size)],
                    vl,
                    vh,
                    T.floormod(vp, page_size),
                ]

    # fmt: off
    @T.prim_func(private=True, s_tir=True)
    def expected(var_pages: T.handle, var_page_table_indptr: T.handle, var_page_table_values: T.handle, var_values: T.handle, seq_id: T.int32):
        T.func_attr({"tirx.is_scheduled": True})
        nhead = T.int32()
        nlayer = T.int32()
        seqlen = T.int32()
        npage = T.int32()
        page_size = T.int32()
        num_total_pages = T.int32()
        num_total_seqs_plus_1 = T.int32()

        pages = T.match_buffer(var_pages, (num_total_pages, nlayer, nhead, page_size), "float16")
        page_table_indptr = T.match_buffer(var_page_table_indptr, (num_total_seqs_plus_1,), "int32")
        page_table_values = T.match_buffer(var_page_table_values, (npage,), "int32")
        values = T.match_buffer(var_values, (nlayer, nhead, seqlen), "float16")

        for ax0_ax1_ax2_fused_0 in T.thread_binding((nlayer * nhead * seqlen + 1023) // 1024, thread="blockIdx.x"):
            for ax0_ax1_ax2_fused_1 in T.thread_binding(1024, thread="threadIdx.x"):
                with T.sblock("block"):
                    v0 = T.axis.spatial(nlayer, (ax0_ax1_ax2_fused_0 * 1024 + ax0_ax1_ax2_fused_1) // (nhead * seqlen))
                    v1 = T.axis.spatial(nhead, (ax0_ax1_ax2_fused_0 * 1024 + ax0_ax1_ax2_fused_1) % (nhead * seqlen) // seqlen)
                    v2 = T.axis.spatial(seqlen, (ax0_ax1_ax2_fused_0 * 1024 + ax0_ax1_ax2_fused_1) % seqlen)
                    T.where(ax0_ax1_ax2_fused_0 * 1024 + ax0_ax1_ax2_fused_1 < nlayer * nhead * seqlen)
                    T.reads(pages[page_table_values[page_table_indptr[seq_id] + v2 // page_size], v0, v1, v2 % page_size], page_table_values[page_table_indptr[seq_id] + v2 // page_size], page_table_indptr[seq_id])
                    T.writes(values[v0, v1, v2])
                    values[v0, v1, v2] = pages[page_table_values[page_table_indptr[seq_id] + v2 // page_size], v0, v1, v2 % page_size]
    # fmt: on

    target = Target("nvidia/geforce-rtx-3090-ti")
    with target:
        mod = tvm.IRModule({"main": func})
        mod = dl.ApplyDefaultSchedule(  # pylint: disable=not-callable
            dl.gpu.Fallback(),
        )(mod)
    assert_structural_equal(mod["main"], expected)


def test_gpu_fallback_ignores_non_gpu_functions():
    @I.ir_module(s_tir=True)
    class Before:
        # This function has no "target" attribute, and is scheduled
        # using the `Target.current`.
        @T.prim_func(s_tir=True)
        def gpu_func(
            A: T.Buffer((1, 32, 1, 128), "float16"),
            C: T.Buffer((1, 1, 4096), "float16"),
        ):
            B = T.sblock_alloc_buffer((1, 1, 32, 128), "float16")
            for i, j, k, l in T.grid(1, 1, 32, 128):
                with T.sblock("T_transpose"):
                    vi, vj, vk, vl = T.axis.remap("SSSS", [i, j, k, l])
                    B[vi, vj, vk, vl] = A[vi, vk, vj, vl]
            for i, j, k in T.grid(1, 1, 4096):
                with T.sblock("T_reshape"):
                    vi, vj, vk = T.axis.remap("SSS", [i, j, k])
                    C[vi, vj, vk] = B[0, 0, vk % 4096 // 128, vk % 128]

        # This function is identical, except that it is explicitly
        # annotated with the "target" attribute, and is scheduled
        # based on the annotation's target.
        @T.prim_func(s_tir=True)
        def cpu_func(
            A: T.Buffer((1, 32, 1, 128), "float16"),
            C: T.Buffer((1, 1, 4096), "float16"),
        ):
            T.func_attr({"target": T.target("llvm")})
            B = T.sblock_alloc_buffer((1, 1, 32, 128), "float16")
            for i, j, k, l in T.grid(1, 1, 32, 128):
                with T.sblock("T_transpose"):
                    vi, vj, vk, vl = T.axis.remap("SSSS", [i, j, k, l])
                    B[vi, vj, vk, vl] = A[vi, vk, vj, vl]
            for i, j, k in T.grid(1, 1, 4096):
                with T.sblock("T_reshape"):
                    vi, vj, vk = T.axis.remap("SSS", [i, j, k])
                    C[vi, vj, vk] = B[0, 0, vk % 4096 // 128, vk % 128]

    @I.ir_module(s_tir=True)
    class After:
        @T.prim_func(s_tir=True)
        def gpu_func(
            A: T.Buffer((1, 32, 1, 128), "float16"),
            C: T.Buffer((1, 1, 4096), "float16"),
        ):
            T.func_attr({"tirx.is_scheduled": True})
            for ax0_fused_0 in T.thread_binding(4, thread="blockIdx.x"):
                for ax0_fused_1 in T.thread_binding(1024, thread="threadIdx.x"):
                    with T.sblock("T_reshape"):
                        v0 = T.axis.spatial(4096, ax0_fused_0 * 1024 + ax0_fused_1)
                        T.reads(A[0, v0 // 128, 0, v0 % 128])
                        T.writes(C[0, 0, v0])
                        C[0, 0, v0] = A[0, v0 // 128, 0, v0 % 128]

        @T.prim_func(s_tir=True)
        def cpu_func(
            A: T.Buffer((1, 32, 1, 128), "float16"),
            C: T.Buffer((1, 1, 4096), "float16"),
        ):
            T.func_attr({"target": T.target("llvm")})
            B = T.sblock_alloc_buffer((1, 1, 32, 128), "float16")
            for i, j, k, l in T.grid(1, 1, 32, 128):
                with T.sblock("T_transpose"):
                    vi, vj, vk, vl = T.axis.remap("SSSS", [i, j, k, l])
                    B[vi, vj, vk, vl] = A[vi, vk, vj, vl]
            for i, j, k in T.grid(1, 1, 4096):
                with T.sblock("T_reshape"):
                    vi, vj, vk = T.axis.remap("SSS", [i, j, k])
                    C[vi, vj, vk] = B[0, 0, vk % 4096 // 128, vk % 128]

    with Target("cuda"):
        mod = dl.ApplyDefaultSchedule(  # pylint: disable=not-callable
            dl.gpu.Fallback(),
        )(Before)
    assert_structural_equal(mod, After)


if __name__ == "__main__":
    tvm.testing.main()
