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


import tvm
import tvm.testing
from tvm.script import tirx as T

target = tvm.target.Target("aws/trn1/trn1.2xlarge")


def lower_and_get_source(func):
    with target:
        mod = tvm.IRModule({"main": func})
        mod = tvm.compile(mod, tir_pipeline="trn")
        src = mod.mod.imports[0].inspect_source()
        return src


def compare_strings_ignore_whitespace(s1, s2):
    # Remove all whitespace by splitting and joining the string back together
    return "".join(s1.split()) == "".join(s2.split())


def test_nki_add_1():
    # fmt: off
    @T.prim_func
    def func(A: T.Buffer((128, 512)), B: T.Buffer((128, 512))):
        T.func_attr({"num_inputs": 1})
        T.device_entry()
        A_sbuf = T.alloc_buffer((128, 512), "float32", scope="trn.sbuf",)
        B_sbuf = T.alloc_buffer((128, 512), "float32", scope="trn.sbuf",)
        with T.attr(0, "tensorized_nki_instruction", 1):
            for i in range(0, 128):
                for j in range(0, 512):
                    T.nki.load(A_sbuf[i, j], A[i, j])
        with T.attr(0, "tensorized_nki_instruction", 1):
            for i in range(0, 128):
                for j in range(0, 512):
                    T.nki.tensorscalar(B_sbuf[i, j], A_sbuf[i, j], T.float32(1.0), "add")
        with T.attr(0, "tensorized_nki_instruction", 1):
            for i in range(0, 128):
                for j in range(0, 512):
                    T.nki.store(B[i, j], B_sbuf[i, j])
        # fmt: on
    src = lower_and_get_source(func)
    print(src)
    expected = """# Function: func_kernel
import neuronxcc.nki.language as nl
from neuronxcc.nki import baremetal, benchmark, simulate_kernel, trace
import numpy as np
import neuronxcc.nki.isa as nisa
import math
import neuronxcc.nki as nki
import neuronxcc.nki.typing as nt
import neuronxcc.nki.compiler as ncc
@nki.compiler.enable_stack_allocator
@nki.compiler.skip_middle_end_transformations
@baremetal(experimental_flags='enable-mutable-parameter', additional_compile_opt='--internal-skip-backend-allocation-opt-nki')
def func_kernel(A_ptr, B_ptr: nt.mutable_tensor, ):
  B_ptr_buffer = B_ptr.reshape([65536])
  A_ptr_buffer = A_ptr.reshape([65536])
  A_sbuf_ptr = nl.ndarray(shape=[128, 512], dtype=np.float32, buffer=ncc.sbuf.mod_alloc(base_addr=0))
  B_sbuf_ptr = nl.ndarray(shape=[128, 512], dtype=np.float32, buffer=ncc.sbuf.mod_alloc(base_addr=2048))
  i = nl.arange(128)
  j = nl.arange(512)
  A_sbuf_ptr[i[:, None, ], j[None, :, ]] = nl.load(A_ptr_buffer[((i[:, None, ] * 512) + j[None, :, ])])
  i_1 = nl.arange(128)
  j_1 = nl.arange(512)
  B_sbuf_ptr[i_1[:, None, ], j_1[None, :, ]] = nisa.tensor_scalar(A_sbuf_ptr[i_1[:, None, ], j_1[None, :, ]], operand0=1.000000e+00, op0=nki.language.add, reverse0=False)
  i_2 = nl.arange(128)
  j_2 = nl.arange(512)
  nl.store(B_ptr_buffer[((i_2[:, None, ] * 512) + j_2[None, :, ])], B_sbuf_ptr[i_2[:, None, ], j_2[None, :, ]])
  return B_ptr
  """  # noqa: E501
    assert compare_strings_ignore_whitespace(src, expected)


def test_nki_add_2():
    # fmt: off
    @T.prim_func
    def func(A: T.Buffer((128, 2048)), B: T.Buffer((128, 2048))):
        T.func_attr({"num_inputs": 1})
        T.device_entry()
        A_sbuf = T.alloc_buffer((128, 512), "float32", scope="trn.sbuf",)
        B_sbuf = T.alloc_buffer((128, 512), "float32", scope="trn.sbuf",)
        for k in range(0, 4):
            with T.attr(0, "tensorized_nki_instruction", 1):
                for i in range(0, 128):
                    for j in range(0, 512):
                        T.nki.load(A_sbuf[i, j], A[i, 512*k+j])
            with T.attr(0, "tensorized_nki_instruction", 1):
                for i in range(0, 128):
                    for j in range(0, 512):
                        T.nki.tensorscalar(B_sbuf[i, j], A_sbuf[i, j], T.float32(1.0), "add")
            with T.attr(0, "tensorized_nki_instruction", 1):
                for i in range(0, 128):
                    for j in range(0, 512):
                        T.nki.store(B[i, 512*k+j], B_sbuf[i, j])

        # fmt: on
    src = lower_and_get_source(func)
    print(src)
    expected = """# Function: func_kernel
import neuronxcc.nki.language as nl
from neuronxcc.nki import baremetal, benchmark, simulate_kernel, trace
import numpy as np
import neuronxcc.nki.isa as nisa
import math
import neuronxcc.nki as nki
import neuronxcc.nki.typing as nt
import neuronxcc.nki.compiler as ncc
@nki.compiler.enable_stack_allocator
@nki.compiler.skip_middle_end_transformations
@baremetal(experimental_flags='enable-mutable-parameter', additional_compile_opt='--internal-skip-backend-allocation-opt-nki')
def func_kernel(A_ptr, B_ptr: nt.mutable_tensor, ):
  B_ptr_buffer = B_ptr.reshape([262144])
  A_ptr_buffer = A_ptr.reshape([262144])
  A_sbuf_ptr = nl.ndarray(shape=[128, 512], dtype=np.float32, buffer=ncc.sbuf.mod_alloc(base_addr=0))
  B_sbuf_ptr = nl.ndarray(shape=[128, 512], dtype=np.float32, buffer=ncc.sbuf.mod_alloc(base_addr=2048))
  for k in nl.sequential_range(4, body_no_reorder=True):
    i = nl.arange(128)
    j = nl.arange(512)
    A_sbuf_ptr[i[:, None, ], j[None, :, ]] = nl.load(A_ptr_buffer[(((i[:, None, ] * 2048) + (k * 512)) + j[None, :, ])])
    i_1 = nl.arange(128)
    j_1 = nl.arange(512)
    B_sbuf_ptr[i_1[:, None, ], j_1[None, :, ]] = nisa.tensor_scalar(A_sbuf_ptr[i_1[:, None, ], j_1[None, :, ]], operand0=1.000000e+00, op0=nki.language.add, reverse0=False)
    i_2 = nl.arange(128)
    j_2 = nl.arange(512)
    nl.store(B_ptr_buffer[(((i_2[:, None, ] * 2048) + (k * 512)) + j_2[None, :, ])], B_sbuf_ptr[i_2[:, None, ], j_2[None, :, ]])
  return B_ptr"""  # noqa: E501
    assert compare_strings_ignore_whitespace(src, expected)


def test_nki_matmul_1():
    TILES_IN_BLOCK_M = 16
    TILES_IN_BLOCK_N = 1
    TILES_IN_BLOCK_K = 8
    TILE_M = 128
    TILE_K = 128
    TILE_N = 512
    K = 1024
    M = 4096
    N = 2048
    BLOCK_M = TILE_M * TILES_IN_BLOCK_M
    BLOCK_N = TILE_N * TILES_IN_BLOCK_N
    BLOCK_K = TILE_K * TILES_IN_BLOCK_K
    # the size has to be multiple of block size
    assert M % BLOCK_M == 0
    assert N % BLOCK_N == 0
    assert K % BLOCK_K == 0

    NUM_BLOCK_M = M // BLOCK_M
    NUM_BLOCK_N = N // BLOCK_N
    NUM_BLOCK_K = K // BLOCK_K

    @T.prim_func
    def func(
        lhsT: T.Buffer((K, M), "float16"),
        rhs: T.Buffer((K, N), "float16"),
        result: T.buffer((M, N), "float16"),
    ):
        T.func_attr({"num_inputs": 2})
        result_tiles = T.alloc_buffer(
            (TILE_M, NUM_BLOCK_M, TILES_IN_BLOCK_M, TILES_IN_BLOCK_N, TILE_N),
            "float32",
            scope="trn.sbuf",
        )
        rhs_tiles = T.alloc_buffer((TILE_K, TILES_IN_BLOCK_K, BLOCK_N), "float16", scope="trn.sbuf")
        lhsT_tiles = T.alloc_buffer(
            (TILE_K, TILES_IN_BLOCK_K, BLOCK_M), "float16", scope="trn.sbuf"
        )
        res_tile = T.alloc_buffer((1, TILE_M, TILE_N), "float32", scope="trn.psum")
        result_packed = T.alloc_buffer((TILE_K, BLOCK_N), "float32", scope="trn.sbuf")
        for n in range(NUM_BLOCK_N):
            with T.attr(0, "tensorized_nki_instruction", 1):
                for i0 in range(TILE_M):
                    for i1 in range(NUM_BLOCK_M):
                        for i2 in range(TILES_IN_BLOCK_M):
                            for i3 in range(TILES_IN_BLOCK_N):
                                for i4 in range(TILE_N):
                                    T.nki.memset(result_tiles[i0, i1, i2, i3, i4], T.float32(0.0))
            for k in range(NUM_BLOCK_K):
                for bk_r in range(TILES_IN_BLOCK_K):
                    with T.attr(0, "tensorized_nki_instruction", 1):
                        for i in range(TILE_K):
                            for j in range(BLOCK_N):
                                T.nki.load(
                                    rhs_tiles[i, bk_r, j],
                                    rhs[
                                        (TILES_IN_BLOCK_K * k + bk_r) * TILE_K + i,
                                        n * BLOCK_N + j,
                                    ],
                                )
                for m in range(NUM_BLOCK_M):
                    for bk_l in range(TILES_IN_BLOCK_K):
                        with T.attr(0, "tensorized_nki_instruction", 1):
                            for i in range(TILE_K):
                                for j in range(BLOCK_M):
                                    T.nki.load(
                                        lhsT_tiles[i, bk_l, j],
                                        lhsT[
                                            (TILES_IN_BLOCK_K * k + bk_l) * TILE_K + i,
                                            m * BLOCK_M + j,
                                        ],
                                    )
                    for bn in range(TILES_IN_BLOCK_N):
                        for bm in range(TILES_IN_BLOCK_M):
                            with T.attr(0, "tensorized_nki_instruction", 1):
                                for i in range(TILE_M):
                                    for j in range(TILE_N):
                                        T.nki.memset(res_tile[0, i, j], T.float32(0.0))
                            for bk in range(TILES_IN_BLOCK_K):
                                with T.attr(0, "tensorized_nki_instruction", 1):
                                    for i in range(TILE_M):
                                        for j in range(TILE_N):
                                            for k in range(TILE_K):
                                                T.nki.matmul(
                                                    res_tile[0, i, j],
                                                    lhsT_tiles[k, bk, bm * TILE_M + i],
                                                    rhs_tiles[k, bk, bn * TILE_N + j],
                                                    1,
                                                )
                            with T.attr(0, "tensorized_nki_instruction", 1):
                                for i in range(TILE_M):
                                    for j in range(TILE_N):
                                        T.nki.tensortensor(
                                            result_tiles[i, m, bm, bn, j],
                                            result_tiles[i, m, bm, bn, j],
                                            res_tile[0, i, j],
                                            "add",
                                        )
            for m in range(NUM_BLOCK_M):
                for bm in range(TILES_IN_BLOCK_M):
                    for bn in range(TILES_IN_BLOCK_N):
                        with T.attr(0, "tensorized_nki_instruction", 1):
                            for i in range(TILE_K):
                                for j in range(TILE_N):
                                    T.nki.tensor_copy(
                                        result_packed[i, bn * TILE_N + j],
                                        result_tiles[i, m, bm, bn, j],
                                    )
                    with T.attr(0, "tensorized_nki_instruction", 1):
                        for i in range(TILE_K):
                            for j in range(BLOCK_N):
                                T.nki.store(
                                    result[m * BLOCK_M + bm * TILE_M + i, n * BLOCK_N + j],
                                    result_packed[i, j],
                                )

    # fmt: on

    src = lower_and_get_source(func)
    print(src)
    expected = """# Function: func_kernel
import neuronxcc.nki.language as nl
from neuronxcc.nki import baremetal, benchmark, simulate_kernel, trace
import numpy as np
import neuronxcc.nki.isa as nisa
import math
import neuronxcc.nki as nki
import neuronxcc.nki.typing as nt
import neuronxcc.nki.compiler as ncc
@nki.compiler.enable_stack_allocator
@nki.compiler.skip_middle_end_transformations
@baremetal(experimental_flags='enable-mutable-parameter', additional_compile_opt='--internal-skip-backend-allocation-opt-nki')
def func_kernel(lhsT_ptr, rhs_ptr, result_ptr: nt.mutable_tensor, ):
  result_ptr_buffer = result_ptr.reshape([8388608])
  rhs_ptr_buffer = rhs_ptr.reshape([2097152])
  lhsT_ptr_buffer = lhsT_ptr.reshape([4194304])
  result_tiles_ptr = nl.ndarray(shape=[128, 2, 16, 1, 512], dtype=np.float32, buffer=ncc.sbuf.mod_alloc(base_addr=0))
  rhs_tiles_ptr = nl.ndarray(shape=[128, 8, 512], dtype=np.float16, buffer=ncc.sbuf.mod_alloc(base_addr=65536))
  lhsT_tiles_ptr = nl.ndarray(shape=[128, 8, 2048], dtype=np.float16, buffer=ncc.sbuf.mod_alloc(base_addr=73728))
  res_tile_ptr = nl.ndarray(shape=[1, nl.par_dim(128), 512], dtype=np.float32, buffer=nl.psum)
  result_packed_ptr = nl.ndarray(shape=[128, 512], dtype=np.float32, buffer=ncc.sbuf.mod_alloc(base_addr=106496))
  for n in nl.sequential_range(4, body_no_reorder=True):
    i0 = nl.arange(128)
    i1 = nl.arange(2)
    i2 = nl.arange(16)
    i4 = nl.arange(512)
    result_tiles_ptr[i0[:, None, None, None, ], i1[None, :, None, None, ], i2[None, None, :, None, ], 0, i4[None, None, None, :, ]] = 0.000000e+00
    for bk_r in nl.sequential_range(8):
      i = nl.arange(128)
      j = nl.arange(512)
      rhs_tiles_ptr[i[:, None, ], bk_r, j[None, :, ]] = nl.load(rhs_ptr_buffer[((((bk_r * 262144) + (i[:, None, ] * 2048)) + (n * 512)) + j[None, :, ])])
    for m in nl.sequential_range(2):
      for bk_l in nl.sequential_range(8):
        i_1 = nl.arange(128)
        j_1 = nl.arange(2048)
        lhsT_tiles_ptr[i_1[:, None, ], bk_l, j_1[None, :, ]] = nl.load(lhsT_ptr_buffer[((((bk_l * 524288) + (i_1[:, None, ] * 4096)) + (m * 2048)) + j_1[None, :, ])])
      for bm in nl.sequential_range(16):
        i_2 = nl.arange(128)
        j_2 = nl.arange(512)
        res_tile_ptr[0, i_2[:, None, ], j_2[None, :, ]] = 0.000000e+00
        for bk in nl.sequential_range(8):
          i_3 = nl.arange(128)
          j_3 = nl.arange(512)
          k = nl.arange(128)
          res_tile_ptr[0, i_3[:, None, ], j_3[None, :, ]] += nisa.nc_matmul(lhsT_tiles_ptr[k[:, None, ], bk, ((bm * 128) + i_3[None, :, ])],rhs_tiles_ptr[k[:, None, ], bk, j_3[None, :, ]])
        i_4 = nl.arange(128)
        j_4 = nl.arange(512)
        result_tiles_ptr[i_4[:, None, ], m, bm, 0, j_4[None, :, ]] = nisa.tensor_tensor(result_tiles_ptr[i_4[:, None, ], m, bm, 0, j_4[None, :, ]], res_tile_ptr[0, i_4[:, None, ], j_4[None, :, ]], op=nki.language.add)
    for m_1 in nl.sequential_range(2):
      for bm_1 in nl.sequential_range(16):
        i_5 = nl.arange(128)
        j_5 = nl.arange(512)
        result_packed_ptr[i_5[:, None, ], j_5[None, :, ]] = nisa.tensor_copy(result_tiles_ptr[i_5[:, None, ], m_1, bm_1, 0, j_5[None, :, ]])
        i_6 = nl.arange(128)
        j_6 = nl.arange(512)
        nl.store(result_ptr_buffer[(((((m_1 * 4194304) + (bm_1 * 262144)) + (i_6[:, None, ] * 2048)) + (n * 512)) + j_6[None, :, ])], result_packed_ptr[i_6[:, None, ], j_6[None, :, ]])
  return result_ptr"""  # noqa: E501
    assert compare_strings_ignore_whitespace(src, expected)


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