// Copyright 2024 Google LLC
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.

$ABC = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
$assert NR % 16 == 0
$assert 16 <= NR <= 64
$assert 1 <= MR <= 16
$assert REQUANTIZATION == "FP32" or not REQUANTIZATION
$assert DATATYPE in ["QD8_F32", "QD8_F16", "QC8", "QC4_F32"]
#include <assert.h>
#if defined(__has_feature)
  #if __has_feature(memory_sanitizer)
    #include <sanitizer/msan_interface.h>
  #endif
#endif

#include <immintrin.h>

#include "xnnpack/common.h"
#include "xnnpack/gemm.h"
#include "xnnpack/intrinsics-polyfill.h"
#include "xnnpack/math.h"
#include "xnnpack/unaligned.h"
$if PREFETCH:
  #include "xnnpack/prefetch.h"


$DATATYPE_SPEC = {"QC8": "qs8_qc8w", "QD8_F16" : "qd8_f16_qc8w", "QD8_F32": "qd8_f32_qc8w", "QC4_F32": "qd8_f32_qc4w"}[DATATYPE]
$REQUANTIZATION_SPEC = "_" + REQUANTIZATION.lower() if REQUANTIZATION else ""
$PARAMS_STRUCT = REQUANTIZATION.lower() + "_scalar" if REQUANTIZATION else "scalar"
$PARAMS_TYPE = {"QC8": "union xnn_qs8_qc8w_conv_minmax_params", "QD8_F16": "union xnn_f16_minmax_params", "QD8_F32": "union xnn_f32_minmax_params", "QC4_F32": "struct xnn_f32_qc4w_minmax_params"}[DATATYPE]
$OUT_T = {"QC8": "int8_t", "QD8_F16": "xnn_float16", "QD8_F32": "float", "QC4_F32": "float"}[DATATYPE]
$_MM_MAX_EPX8 = "_mm_max_epi8"
$_MM512_CVTXEPI32_EPI8 = "_mm512_cvtsepi32_epi8"
void xnn_${DATATYPE_SPEC}_gemm_minmax${REQUANTIZATION_SPEC}_ukernel_${MR}x${NR}c4__avx512amx${"_prfm" if PREFETCH else ""}(
    size_t mr,
    size_t nc,
    size_t kc,
    const int8_t* restrict a,
    size_t a_stride,
    const void* restrict w,
    ${OUT_T}* restrict c,
    size_t cm_stride,
    size_t cn_stride,
    $if DATATYPE in ["QD8_F16", "QD8_F32", "QC4_F16", "QC4_F32"]:
      const ${PARAMS_TYPE} params[restrict XNN_MIN_ELEMENTS(1)],
      const struct xnn_qd8_quantization_params quantization_params[restrict XNN_MIN_ELEMENTS(1)])
    $else:
      const ${PARAMS_TYPE} params[restrict XNN_MIN_ELEMENTS(1)])
{
  assert(mr != 0);
  assert(mr <= ${MR});
  assert(nc != 0);
  assert(kc != 0);
  assert(kc % sizeof(int8_t) == 0);
  assert(a != NULL);
  assert(w != NULL);
  assert(c != NULL);

// TODO: amxintrin.h only provide intrinsics for __x86_64__
// Update if amxintrin changes
#if defined(__x86_64__)
  $if DATATYPE in ["QC4_F32", "QC4_F16"]:
    __attribute__((aligned(64))) int8_t weight_buffer[16 * 64];
  __attribute__((aligned(64))) int32_t res[${NR // 16}][${MR} * 16];

  kc = round_up_po2(kc, 4 * sizeof(int8_t));
  const size_t kremainder = (kc & 63) ? (kc & 63) : 64;

  // Define tile config data structure
  struct __tile_config {
    uint8_t palette_id;
    uint8_t start_row;
    uint8_t reserved_0[14];
    uint16_t colsb[8];
    uint16_t reserved_1[8];
    uint8_t rows[8];
    uint8_t reserved_2[8];
  };

  // Load tile configuration
  __attribute__((aligned(64))) struct __tile_config tile_data = {0};
  tile_data.palette_id = 1;
  tile_data.rows[0] = mr;              // tmm0 = res[0]
  tile_data.rows[1] = mr;              // tmm1 = res[1]
  tile_data.rows[2] = mr;              // tmm2 = res[2]
  tile_data.rows[3] = mr;              // tmm3 = res[3]
  tile_data.rows[4] = mr;              // tmm4 = input
  tile_data.rows[5] = 16;              // tmm5 = weights
  tile_data.rows[6] = mr;              // tmm6 = input remainder
  tile_data.rows[7] = kremainder >> 2; // tmm7 = weights remainder

  tile_data.colsb[0] = 64;          // tmm0 = res[0]
  tile_data.colsb[1] = 64;          // tmm1 = res[1]
  tile_data.colsb[2] = 64;          // tmm2 = res[2]
  tile_data.colsb[3] = 64;          // tmm3 = res[3]
  tile_data.colsb[4] = 64;          // tmm4 = input
  tile_data.colsb[5] = 64;          // tmm5 = weights
  tile_data.colsb[6] = kremainder;  // tmm6 = input remainder
  tile_data.colsb[7] = 64;          // tmm7 = weights remainder

  //_tile_loadconfig(&tile_data);
  __asm__ volatile ("ldtilecfg %0" :: "m" (tile_data));

  ${OUT_T}* c0 = c;
  $for M in range(1, MR):
    ${OUT_T}* c${M} = (${OUT_T}*) ((uintptr_t) c${M-1} + cm_stride);
    $if M % 2 == 0:
      if XNN_UNPREDICTABLE(mr <= ${M}) {
        c${M} = c${M-1};
      }
    $elif M + 1 == MR:
      if XNN_UNPREDICTABLE(mr != ${M+1}) {
        c${M} = c${M-1};
      }
    $else:
      if XNN_UNPREDICTABLE(mr < ${M+1}) {
        c${M} = c${M-1};
      }

  $if DATATYPE in ["QD8_F16", "QD8_F32", "QC4_F16", "QC4_F32"]:
    $if "F16" in DATATYPE:
      const __m512 voutput_min = _mm512_cvtph_ps(_mm256_set1_epi16(*(const uint16_t*) &params->${PARAMS_STRUCT}.min));
      const __m512 voutput_max = _mm512_cvtph_ps(_mm256_set1_epi16(*(const uint16_t*) &params->${PARAMS_STRUCT}.max));
    $else:
      const __m512 voutput_min = _mm512_set1_ps(params->${PARAMS_STRUCT}.min);
      const __m512 voutput_max = _mm512_set1_ps(params->${PARAMS_STRUCT}.max);
    // XNN_FORCE_REALIZATION(voutput_min);
    // XNN_FORCE_REALIZATION(voutput_max);
    $if DATATYPE in ["QC4_F32", "QC4_F16"]:
      const __m512i vmask = _mm512_set1_epi8(0xF0);
      const __m512i vshl4 = _mm512_set1_epi64(0x01020408);
      XNN_FORCE_REALIZATION(vmask);
      XNN_FORCE_REALIZATION(vshl4);
  $else:
    const __m512 voutput_max_less_zero_point = _mm512_set1_ps((int32_t) params->${PARAMS_STRUCT}.output_max - (int32_t) params->${PARAMS_STRUCT}.output_zero_point);
    const __m512i voutput_zero_point = _mm512_set1_epi32(params->${PARAMS_STRUCT}.output_zero_point);
    const __m128i voutput_min = _mm_set1_epi8(params->${PARAMS_STRUCT}.output_min);
    // XNN_FORCE_REALIZATION(voutput_max_less_zero_point);
    // XNN_FORCE_REALIZATION(voutput_zero_point);
    // XNN_FORCE_REALIZATION(voutput_min);

  do {
    $for N in range(0, NR, 16):
      const __m512i vksum${ABC[N:N+16]} = _mm512_load_epi32((const int32_t*) w + ${N});
    w = (const int32_t*) w + ${NR};

    // Zero tile accumulator
    __asm__ volatile (
      $for N in range(0, NR, 16):
        "tilezero %%tmm${N // 16}\\n"
      ::);

    size_t k = kc;
    while (k >= 64 * sizeof(int8_t)) {
      _tile_loadd(4, a, a_stride);
      a += 64;
      $for N in range(0, NR, 16):
        $if DATATYPE in ["QC4_F32", "QC4_F16"]:
          $for K in range(8):
            const __m512i vb${K}x${ABC[N:N+16]} = _mm512_load_epi32((const int8_t*) w + ${N * 4 + NR * K * 4});
          $for K in range(8):
            const __m512i vl${K}x${ABC[N:N+16]} = _mm512_gf2p8affine_epi64_epi8(vb${K}x${ABC[N:N+16]}, vshl4, 0);
            const __m512i vh${K}x${ABC[N:N+16]} = _mm512_and_si512(vb${K}x${ABC[N:N+16]}, vmask);
          $for K in range(8):
            _mm512_store_epi32(weight_buffer + ${64*K*2+0}, vl${K}x${ABC[N:N+16]});
            _mm512_store_epi32(weight_buffer + ${64*K*2+64}, vh${K}x${ABC[N:N+16]});
          _tile_loadd(5, weight_buffer, 64);
        $else:
          _tile_loadd(5, (const int8_t*) w + ${N * 4}, ${NR * 4});
        _tile_dpbssd(${N // 16}, 4, 5);
      $if PREFETCH:
        $for P in range(4096, 5120 + NR // 16 * 1024, 64):
          xnn_prefetch_to_l1((const int8_t*) w + ${P});

      $if DATATYPE in ["QC4_F32", "QC4_F16"]:
        w = (const int8_t*) w + ${NR // 16 * 512};
      $else:
        w = (const int8_t*) w + ${NR // 16 * 1024};
      k -= 64 * sizeof(int8_t);
    }

    if XNN_UNLIKELY(k != 0) {
      _tile_loadd(6, a, a_stride);
      a += kremainder;
      $for N in range(0, NR, 16):
        $if DATATYPE in ["QC4_F32", "QC4_F16"]:
          for (size_t k = 0; k < ((kremainder + 7) >> 3); ++k) {
            const __m512i vb${ABC[N:N+16]} = _mm512_load_epi32((const int8_t*) w + ${N * 4} + ${NR * 4} * k);
            const __m512i vl${ABC[N:N+16]} = _mm512_gf2p8affine_epi64_epi8(vb${ABC[N:N+16]}, vshl4, 0);
            const __m512i vh${ABC[N:N+16]} = _mm512_and_si512(vb${ABC[N:N+16]}, vmask);
            _mm512_store_epi32(weight_buffer + ${64*2} * k + 0, vl${ABC[N:N+16]});
            _mm512_store_epi32(weight_buffer + ${64*2} * k + 64, vh${ABC[N:N+16]});
          }
          _tile_loadd(7, weight_buffer, 64);
        $else:
          _tile_loadd(7, (const int8_t*) w + ${N * 4}, ${NR * 4});
        _tile_dpbssd(${N // 16}, 6, 7);

      $if DATATYPE in ["QC4_F32", "QC4_F16"]:
        w = (const int8_t*) w + ((kremainder + 7) >> 3) * ${NR * 4};
      $else:
        w = (const int8_t*) w + kremainder * ${NR};
      k -= kremainder * sizeof(int8_t);
    }

    // TODO: Instead of processing up to 4 tiles (16x64) consider
    // quantizing 1 tile at a time (16 registers)
    $for N in range(0, NR, 16):
      _tile_stored(${N // 16}, &res[${N // 16}][0], 64);

    // TODO: Fix msan for AMX
    #if defined(__has_feature)
      #if __has_feature(memory_sanitizer)
        __msan_unpoison(res, sizeof(res));
      #endif
    #endif

    // TODO: Instead of processing up to 4 tiles (16x64) consider
    // quantizing 1 row at a time.
    $if DATATYPE in ["QD8_F16", "QD8_F32", "QC4_F16", "QC4_F32"]:
      $for M in range(MR):
        $for N in range(0, NR, 16):
          __m512i vacc${M}x${ABC[N:N+16]} = _mm512_mullo_epi32(vksum${ABC[N:N+16]}, _mm512_set1_epi32((int) quantization_params[${M}].zero_point));
      // Add tile to bias
      $for M in range(MR):
        $for N in range(0, NR, 16):
          vacc${M}x${ABC[N:N+16]} = _mm512_add_epi32(vacc${M}x${ABC[N:N+16]}, _mm512_load_epi32(&res[${N // 16}][0] + ${M * 16}));
    $else:
      // Add tile to bias
      $for M in range(MR):
        $for N in range(0, NR, 16):
          __m512i vacc${M}x${ABC[N:N+16]} = _mm512_add_epi32(vksum${ABC[N:N+16]}, _mm512_load_epi32(&res[${N // 16}][0] + ${M * 16}));

    $if DATATYPE in ["QC4_F32", "QC4_F16"]:
      $for M in range(MR):
        $for N in range(0, NR, 16):
          vacc${M}x${ABC[N:N+16]} = _mm512_srai_epi32(vacc${M}x${ABC[N:N+16]}, 4);
    $for M in range(MR):
      $for N in range(0, NR, 16):
        __m512 vscaled${M}x${ABC[N:N+16]} = _mm512_cvtepi32_ps(vacc${M}x${ABC[N:N+16]});

    $if DATATYPE in ["QD8_F16", "QD8_F32", "QC4_F16", "QC4_F32"]:
      $for M in range(MR):
        $for N in range(0, NR, 16):
          vscaled${M}x${ABC[N:N+16]} = _mm512_mul_ps(vscaled${M}x${ABC[N:N+16]}, _mm512_set1_ps(quantization_params[${M}].inv_scale));

      $for N in range(0, NR, 16):
        const __m512 vfilter_output_scale${ABC[N:N+16]} = _mm512_load_ps((const float*) w + ${N});
      w = (const int32_t*) w + ${NR};
      $for N in range(0, NR, 16):
        const __m512 vbias${ABC[N:N+16]} = _mm512_load_ps((const float*) w + ${N});
      w = (const int32_t*) w + ${NR};

      $for M in range(MR):
        $for N in range(0, NR, 16):
          vscaled${M}x${ABC[N:N+16]} = _mm512_fmadd_ps(vscaled${M}x${ABC[N:N+16]}, vfilter_output_scale${ABC[N:N+16]}, vbias${ABC[N:N+16]});

      $for M in range(MR):
        $for N in range(0, NR, 16):
          vscaled${M}x${ABC[N:N+16]} = _mm512_max_ps(vscaled${M}x${ABC[N:N+16]}, voutput_min);

      $for M in range(MR):
        $for N in range(0, NR, 16):
          vscaled${M}x${ABC[N:N+16]} = _mm512_min_ps(vscaled${M}x${ABC[N:N+16]}, voutput_max);

      $if DATATYPE in ["QC4_F16", "QD8_F16"]:
        $for M in range(MR):
          $for N in range(0, NR, 16):
            __m256i vfp16out${M}x${ABC[N:N+16]} = _mm512_cvtps_ph(vscaled${M}x${ABC[N:N+16]}, _MM_FROUND_TO_NEAREST_INT);
        if XNN_LIKELY(nc >= ${NR}) {
          $for M in reversed(range(MR)):
            $for N in range(0, NR, 16):
              _mm256_storeu_si256((__m256i*) (c${M} + ${N}), vfp16out${M}x${ABC[N:N+16]});
            c${M} = (${OUT_T}*) ((uintptr_t) c${M} + cn_stride);

          a -= kc;
          nc -= ${NR};
        } else {
          // Prepare mask for valid 32-bit elements (depends on nc).
          $for N in range(0, NR, 16):
            const __mmask16 vmask${N // 16} = _cvtu32_mask16((uint32_t) ((((UINT64_C(1) << nc) - 1) >> ${N}) & 0xFFFF));
          $for M in reversed(range(MR)):
            $for N in range(0, NR, 16):
              _mm256_mask_storeu_epi16(c${M} + ${N}, vmask${N // 16}, vfp16out${M}x${ABC[N:N+16]});
          nc = 0;
        }
      $else:
        if XNN_LIKELY(nc >= ${NR}) {
          $for M in reversed(range(MR)):
            $for N in range(0, NR, 16):
              _mm512_storeu_ps(c${M} + ${N}, vscaled${M}x${ABC[N:N+16]});
            c${M} = (${OUT_T}*) ((uintptr_t) c${M} + cn_stride);

          a -= kc;
          nc -= ${NR};
        } else {
          // Prepare mask for valid 32-bit elements (depends on nc).
          $for N in range(0, NR, 16):
            const __mmask16 vmask${N // 16} = _cvtu32_mask16((uint32_t) ((((UINT64_C(1) << nc) - 1) >> ${N}) & 0xFFFF));
          $for M in reversed(range(MR)):
            $for N in range(0, NR, 16):
              _mm512_mask_storeu_ps(c${M} + ${N}, vmask${N // 16}, vscaled${M}x${ABC[N:N+16]});
          nc = 0;
        }
    $elif DATATYPE == "QC8":
      $for N in range(0, NR, 16):
        const __m512 vscale${ABC[N:N+16]} = _mm512_load_ps((const float*) w + ${N});
      w = (const int32_t*) w + ${NR};

      $for M in range(MR):
        $for N in range(0, NR, 16):
          vscaled${M}x${ABC[N:N+16]} = _mm512_mul_ps(vscaled${M}x${ABC[N:N+16]}, vscale${ABC[N:N+16]});

      $for M in range(MR):
        $for N in range(0, NR, 16):
          vscaled${M}x${ABC[N:N+16]} = _mm512_min_ps(vscaled${M}x${ABC[N:N+16]}, voutput_max_less_zero_point);

      $for M in range(MR):
        $for N in range(0, NR, 16):
          vacc${M}x${ABC[N:N+16]} = _mm512_cvtps_epi32(vscaled${M}x${ABC[N:N+16]});

      $for M in range(MR):
        $for N in range(0, NR, 16):
          vacc${M}x${ABC[N:N+16]} = _mm512_add_epi32(vacc${M}x${ABC[N:N+16]}, voutput_zero_point);

      $for M in range(MR):
        $for N in range(0, NR, 16):
          __m128i vout${M}x${ABC[N:N+16]} = ${_MM512_CVTXEPI32_EPI8}(vacc${M}x${ABC[N:N+16]});

      $for M in range(MR):
        $for N in range(0, NR, 16):
          vout${M}x${ABC[N:N+16]} = ${_MM_MAX_EPX8}(vout${M}x${ABC[N:N+16]}, voutput_min);

      if XNN_LIKELY(nc >= ${NR}) {
        $for M in reversed(range(MR)):
          $for N in range(0, NR, 16):
            _mm_storeu_si128((__m128i*) (c${M} + ${N}), vout${M}x${ABC[N:N+16]});
          c${M} = (${OUT_T}*) ((uintptr_t) c${M} + cn_stride);
        a -= kc;
        nc -= ${NR};
      } else {
        // Prepare mask for valid 8-bit elements (depends on nc).
        $for N in range(0, NR, 16):
          const __mmask16 vmask${N // 16} = _cvtu32_mask16((uint32_t) ((((UINT64_C(1) << nc) - 1) >> ${N}) & 0xFFFF));

        $for M in reversed(range(MR)):
          $for N in range(0, NR, 16):
            _mm_mask_storeu_epi8(c${M} + ${N}, vmask${N // 16}, vout${M}x${ABC[N:N+16]});
        nc = 0;
      }
  } while (nc != 0);
  // Release tile config
  //  _tile_release();
  __asm__ volatile ("tilerelease" ::);
  #endif  // defined(__x86_64__)
}
