/*
 * Copyright (c) 2023, NVIDIA CORPORATION.
 *
 * 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 <cooperative_groups.h>

#include <nv_gpu_cache.hpp>

namespace cg = cooperative_groups;

// Overload CUDA atomic for other 64bit unsinged/signed integer type
__forceinline__ __device__ long atomicAdd(long* address, long val) {
  return (long)atomicAdd((unsigned long long*)address, (unsigned long long)val);
}

__forceinline__ __device__ long long atomicAdd(long long* address, long long val) {
  return (long long)atomicAdd((unsigned long long*)address, (unsigned long long)val);
}

__forceinline__ __device__ unsigned long atomicAdd(unsigned long* address, unsigned long val) {
  return (unsigned long)atomicAdd((unsigned long long*)address, (unsigned long long)val);
}

namespace gpu_cache {

#ifdef LIBCUDACXX_VERSION
template <int warp_size>
__forceinline__ __device__ void warp_tile_copy(const size_t lane_idx,
                                               const size_t emb_vec_size_in_float, float* d_dst,
                                               const float* d_src) {
#pragma unroll
  for (size_t i = lane_idx; i < emb_vec_size_in_float; i += warp_size) {
    d_dst[i] = d_src[i];
  }
}
#else
template <int warp_size>
__forceinline__ __device__ void warp_tile_copy(const size_t lane_idx,
                                               const size_t emb_vec_size_in_float,
                                               volatile float* d_dst, volatile float* d_src) {

#pragma unroll
  for (size_t i = lane_idx; i < emb_vec_size_in_float; i += warp_size) {
    d_dst[i] = d_src[i];
  }
}
#endif

#ifdef LIBCUDACXX_VERSION
// Will be called by multiple thread_block_tile((sub-)warp) on the same mutex
// Expect only one thread_block_tile return to execute critical section at any time
template <typename mutex, int warp_size>
__forceinline__ __device__ void warp_lock_mutex(const cg::thread_block_tile<warp_size>& warp_tile,
                                                mutex& set_mutex) {
  // The first thread of this (sub-)warp to acquire the lock
  if (warp_tile.thread_rank() == 0) {
    set_mutex.acquire();
  }
  warp_tile.sync();  // Synchronize the threads in the (sub-)warp. Execution barrier + memory fence
}

// The (sub-)warp holding the mutex will unlock the mutex after finishing the critical section on a
// set Expect any following (sub-)warp that acquire the mutex can see its modification done in the
// critical section
template <typename mutex, int warp_size>
__forceinline__ __device__ void warp_unlock_mutex(const cg::thread_block_tile<warp_size>& warp_tile,
                                                  mutex& set_mutex) {
  warp_tile.sync();  // Synchronize the threads in the (sub-)warp. Execution barrier + memory fence
  // The first thread of this (sub-)warp to release the lock
  if (warp_tile.thread_rank() == 0) {
    set_mutex.release();
  }
}
#else
// Will be called by multiple thread_block_tile((sub-)warp) on the same mutex
// Expect only one thread_block_tile return to execute critical section at any time
template <int warp_size>
__forceinline__ __device__ void warp_lock_mutex(const cg::thread_block_tile<warp_size>& warp_tile,
                                                volatile int& set_mutex) {
  // The first thread of this (sub-)warp to acquire the lock
  if (warp_tile.thread_rank() == 0) {
    while (0 == atomicCAS((int*)&set_mutex, 1, 0))
      ;
  }
  __threadfence();
  warp_tile.sync();  // Synchronize the threads in the (sub-)warp. Execution barrier + memory fence
}

// The (sub-)warp holding the mutex will unlock the mutex after finishing the critical section on a
// set Expect any following (sub-)warp that acquire the mutex can see its modification done in the
// critical section
template <int warp_size>
__forceinline__ __device__ void warp_unlock_mutex(const cg::thread_block_tile<warp_size>& warp_tile,
                                                  volatile int& set_mutex) {
  __threadfence();
  warp_tile.sync();  // Synchronize the threads in the (sub-)warp. Execution barrier + memory fence
  // The first thread of this (sub-)warp to release the lock
  if (warp_tile.thread_rank() == 0) {
    atomicExch((int*)&set_mutex, 1);
  }
}
#endif

// The (sub-)warp doing all reduction to find the slot with min slot_counter
// The slot with min slot_counter is the LR slot.
template <typename ref_counter_type, int warp_size>
__forceinline__ __device__ void warp_min_reduction(
    const cg::thread_block_tile<warp_size>& warp_tile, ref_counter_type& min_slot_counter_val,
    size_t& slab_distance, size_t& slot_distance) {
  const size_t lane_idx = warp_tile.thread_rank();
  slot_distance = lane_idx;

  for (size_t i = (warp_tile.size() >> 1); i > 0; i = i >> 1) {
    ref_counter_type input_slot_counter_val = warp_tile.shfl_xor(min_slot_counter_val, (int)i);
    size_t input_slab_distance = warp_tile.shfl_xor(slab_distance, (int)i);
    size_t input_slot_distance = warp_tile.shfl_xor(slot_distance, (int)i);

    if (input_slot_counter_val == min_slot_counter_val) {
      if (input_slab_distance == slab_distance) {
        if (input_slot_distance < slot_distance) {
          slot_distance = input_slot_distance;
        }
      } else if (input_slab_distance < slab_distance) {
        slab_distance = input_slab_distance;
        slot_distance = input_slot_distance;
      }
    } else if (input_slot_counter_val < min_slot_counter_val) {
      min_slot_counter_val = input_slot_counter_val;
      slab_distance = input_slab_distance;
      slot_distance = input_slot_distance;
    }
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////////

#ifdef LIBCUDACXX_VERSION
// Kernel to initialize the GPU cache
// Init every entry of the cache with <unused_key, value> pair
template <typename slabset, typename ref_counter_type, typename atomic_ref_counter_type,
          typename key_type, typename mutex>
__global__ void init_cache(slabset* keys, ref_counter_type* slot_counter,
                           atomic_ref_counter_type* global_counter, const size_t num_slot,
                           const key_type empty_key, mutex* set_mutex,
                           const size_t capacity_in_set) {
  const size_t idx = blockIdx.x * blockDim.x + threadIdx.x;
  if (idx < num_slot) {
    // Set the key of this slot to unused key
    // Flatten the cache
    key_type* key_slot = (key_type*)keys;
    key_slot[idx] = empty_key;

    // Clear the counter for this slot
    slot_counter[idx] = 0;
  }
  // First CUDA thread clear the global counter
  if (idx == 0) {
    new (global_counter) atomic_ref_counter_type(0);
  }

  // First capacity_in_set CUDA thread initialize mutex
  if (idx < capacity_in_set) {
    new (set_mutex + idx) mutex(1);
  }
}

template <typename atomic_ref_counter_type, typename mutex>
__global__ void destruct_kernel(atomic_ref_counter_type* global_counter, mutex* set_mutex,
                                const size_t capacity_in_set) {
  const size_t idx = blockIdx.x * blockDim.x + threadIdx.x;
  // First CUDA thread destruct the global_counter
  if (idx == 0) {
    global_counter->~atomic_ref_counter_type();
  }
  // First capacity_in_set CUDA thread destruct the set mutex
  if (idx < capacity_in_set) {
    (set_mutex + idx)->~mutex();
  }
}
#else
// Kernel to initialize the GPU cache
// Init every entry of the cache with <unused_key, value> pair
template <typename slabset, typename ref_counter_type, typename key_type>
__global__ void init_cache(slabset* keys, ref_counter_type* slot_counter,
                           ref_counter_type* global_counter, const size_t num_slot,
                           const key_type empty_key, int* set_mutex, const size_t capacity_in_set) {
  const size_t idx = blockIdx.x * blockDim.x + threadIdx.x;
  if (idx < num_slot) {
    // Set the key of this slot to unused key
    // Flatten the cache
    key_type* key_slot = (key_type*)keys;
    key_slot[idx] = empty_key;

    // Clear the counter for this slot
    slot_counter[idx] = 0;
  }
  // First CUDA thread clear the global counter
  if (idx == 0) {
    global_counter[idx] = 0;
  }

  // First capacity_in_set CUDA thread initialize mutex
  if (idx < capacity_in_set) {
    set_mutex[idx] = 1;
  }
}
#endif

// Kernel to update global counter
// Resolve distance overflow issue as well
#ifdef LIBCUDACXX_VERSION
template <typename atomic_ref_counter_type>
__global__ void update_kernel_overflow_ignore(atomic_ref_counter_type* global_counter,
                                              size_t* d_missing_len) {
  // Update global counter
  global_counter->fetch_add(1, cuda::std::memory_order_relaxed);
  *d_missing_len = 0;
}
#else
template <typename ref_counter_type>
__global__ void update_kernel_overflow_ignore(ref_counter_type* global_counter,
                                              size_t* d_missing_len) {
  // Update global counter
  atomicAdd(global_counter, 1);
  *d_missing_len = 0;
}
#endif

#ifdef LIBCUDACXX_VERSION
// Kernel to read from cache
// Also update locality information for touched slot
template <typename key_type, typename ref_counter_type, typename atomic_ref_counter_type,
          typename slabset, typename set_hasher, typename slab_hasher, typename mutex,
          key_type empty_key, int set_associativity, int warp_size>
__global__ void get_kernel(const key_type* d_keys, const size_t len, float* d_values,
                           const size_t embedding_vec_size, uint64_t* d_missing_index,
                           key_type* d_missing_keys, size_t* d_missing_len,
                           const atomic_ref_counter_type* global_counter,
                           ref_counter_type* slot_counter, const size_t capacity_in_set,
                           const slabset* keys, const float* vals, mutex* set_mutex,
                           const size_t task_per_warp_tile) {
  // Lane(thread) ID within a warp_tile
  cg::thread_block_tile<warp_size> warp_tile =
      cg::tiled_partition<warp_size>(cg::this_thread_block());
  const size_t lane_idx = warp_tile.thread_rank();
  // Warp tile global ID
  const size_t warp_tile_global_idx =
      (blockIdx.x * (blockDim.x / warp_size)) + warp_tile.meta_group_rank();
  // The index of key for this thread
  const size_t key_idx = (warp_tile_global_idx * task_per_warp_tile) + lane_idx;
  // The assigned key for this lane(thread)
  key_type key;
  // The dst slabset and the dst slab inside this set
  size_t src_set;
  size_t src_slab;
  // The variable that contains the missing key
  key_type missing_key;
  // The variable that contains the index for the missing key
  uint64_t missing_index;
  // The counter for counting the missing key in this warp
  uint8_t warp_missing_counter = 0;
  // Active flag: whether current lane(thread) has unfinished task
  bool active = false;
  if (lane_idx < task_per_warp_tile) {
    if (key_idx < len) {
      active = true;
      key = d_keys[key_idx];
      src_set = set_hasher::hash(key) % capacity_in_set;
      src_slab = slab_hasher::hash(key) % set_associativity;
    }
  }

  // Lane participate in warp_tile ballot to produce warp-level work queue
  unsigned active_mask = warp_tile.ballot(active);

  // The warp-level outer loop: finish all the tasks within the work queue
  while (active_mask != 0) {
    // Next task in the work quere, start from lower index lane(thread)
    int next_lane = __ffs(active_mask) - 1;
    // Broadcast the task and the global index to all lane in the warp_tile
    key_type next_key = warp_tile.shfl(key, next_lane);
    size_t next_idx = warp_tile.shfl(key_idx, next_lane);
    size_t next_set = warp_tile.shfl(src_set, next_lane);
    size_t next_slab = warp_tile.shfl(src_slab, next_lane);

    // Counter to record how many slab have been searched
    size_t counter = 0;

    // Working queue before task started
    const unsigned old_active_mask = active_mask;

    // Lock the slabset before operating the slabset
    warp_lock_mutex<mutex, warp_size>(warp_tile, set_mutex[next_set]);

    // The warp-level inner loop: finish a single task in the work queue
    while (active_mask == old_active_mask) {
      // When all the slabs inside a slabset have been searched, mark missing task, task is
      // completed
      if (counter >= set_associativity) {
        if (lane_idx == warp_missing_counter) {
          missing_key = next_key;
          missing_index = next_idx;
        }

        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        warp_missing_counter++;
        active_mask = warp_tile.ballot(active);
        break;
      }

      // The warp_tile read out the slab
      key_type read_key = keys[next_set].set_[next_slab].slab_[lane_idx];

      // Compare the slab data with the target key
      int found_lane = __ffs(warp_tile.ballot(read_key == next_key)) - 1;

      // If found, mark hit task, copy the founded data, the task is completed
      if (found_lane >= 0) {
        size_t found_offset = (next_set * set_associativity + next_slab) * warp_size + found_lane;
        if (lane_idx == (size_t)next_lane) {
          slot_counter[found_offset] = global_counter->load(cuda::std::memory_order_relaxed);
          active = false;
        }

        warp_tile_copy<warp_size>(lane_idx, embedding_vec_size,
                                  d_values + next_idx * embedding_vec_size,
                                  vals + found_offset * embedding_vec_size);

        active_mask = warp_tile.ballot(active);
        break;
      }

      // Compare the slab data with empty key, if found empty key, mark missing task, task is
      // completed
      if (warp_tile.ballot(read_key == empty_key) != 0) {
        if (lane_idx == warp_missing_counter) {
          missing_key = next_key;
          missing_index = next_idx;
        }

        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        warp_missing_counter++;
        active_mask = warp_tile.ballot(active);
        break;
      }

      // Not found in this slab, the task is not completed, goto searching next slab
      counter++;
      next_slab = (next_slab + 1) % set_associativity;
    }

    // Unlock the slabset after operating the slabset
    warp_unlock_mutex<mutex, warp_size>(warp_tile, set_mutex[next_set]);
  }

  // After warp_tile complete the working queue, save the result for output
  // First thread of the warp_tile accumulate the missing length to global variable
  size_t warp_position;
  if (lane_idx == 0) {
    warp_position = atomicAdd(d_missing_len, (size_t)warp_missing_counter);
  }
  warp_position = warp_tile.shfl(warp_position, 0);

  if (lane_idx < warp_missing_counter) {
    d_missing_keys[warp_position + lane_idx] = missing_key;
    d_missing_index[warp_position + lane_idx] = missing_index;
  }
}
#else
// Kernel to read from cache
// Also update locality information for touched slot
template <typename key_type, typename ref_counter_type, typename slabset, typename set_hasher,
          typename slab_hasher, key_type empty_key, int set_associativity, int warp_size>
__global__ void get_kernel(const key_type* d_keys, const size_t len, float* d_values,
                           const size_t embedding_vec_size, uint64_t* d_missing_index,
                           key_type* d_missing_keys, size_t* d_missing_len,
                           ref_counter_type* global_counter,
                           volatile ref_counter_type* slot_counter, const size_t capacity_in_set,
                           volatile slabset* keys, volatile float* vals, volatile int* set_mutex,
                           const size_t task_per_warp_tile) {
  // Lane(thread) ID within a warp_tile
  cg::thread_block_tile<warp_size> warp_tile =
      cg::tiled_partition<warp_size>(cg::this_thread_block());
  const size_t lane_idx = warp_tile.thread_rank();
  // Warp tile global ID
  const size_t warp_tile_global_idx =
      (blockIdx.x * (blockDim.x / warp_size)) + warp_tile.meta_group_rank();
  // The index of key for this thread
  const size_t key_idx = (warp_tile_global_idx * task_per_warp_tile) + lane_idx;
  // The assigned key for this lane(thread)
  key_type key;
  // The dst slabset and the dst slab inside this set
  size_t src_set;
  size_t src_slab;
  // The variable that contains the missing key
  key_type missing_key;
  // The variable that contains the index for the missing key
  uint64_t missing_index;
  // The counter for counting the missing key in this warp
  uint8_t warp_missing_counter = 0;
  // Active flag: whether current lane(thread) has unfinished task
  bool active = false;
  if (lane_idx < task_per_warp_tile) {
    if (key_idx < len) {
      active = true;
      key = d_keys[key_idx];
      src_set = set_hasher::hash(key) % capacity_in_set;
      src_slab = slab_hasher::hash(key) % set_associativity;
    }
  }

  // Lane participate in warp_tile ballot to produce warp-level work queue
  unsigned active_mask = warp_tile.ballot(active);

  // The warp-level outer loop: finish all the tasks within the work queue
  while (active_mask != 0) {
    // Next task in the work quere, start from lower index lane(thread)
    int next_lane = __ffs(active_mask) - 1;
    // Broadcast the task and the global index to all lane in the warp_tile
    key_type next_key = warp_tile.shfl(key, next_lane);
    size_t next_idx = warp_tile.shfl(key_idx, next_lane);
    size_t next_set = warp_tile.shfl(src_set, next_lane);
    size_t next_slab = warp_tile.shfl(src_slab, next_lane);

    // Counter to record how many slab have been searched
    size_t counter = 0;

    // Working queue before task started
    const unsigned old_active_mask = active_mask;

    // Lock the slabset before operating the slabset
    warp_lock_mutex<warp_size>(warp_tile, set_mutex[next_set]);

    // The warp-level inner loop: finish a single task in the work queue
    while (active_mask == old_active_mask) {
      // When all the slabs inside a slabset have been searched, mark missing task, task is
      // completed
      if (counter >= set_associativity) {
        if (lane_idx == warp_missing_counter) {
          missing_key = next_key;
          missing_index = next_idx;
        }

        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        warp_missing_counter++;
        active_mask = warp_tile.ballot(active);
        break;
      }

      // The warp_tile read out the slab
      key_type read_key = ((volatile key_type*)(keys[next_set].set_[next_slab].slab_))[lane_idx];

      // Compare the slab data with the target key
      int found_lane = __ffs(warp_tile.ballot(read_key == next_key)) - 1;

      // If found, mark hit task, copy the founded data, the task is completed
      if (found_lane >= 0) {
        size_t found_offset = (next_set * set_associativity + next_slab) * warp_size + found_lane;
        if (lane_idx == (size_t)next_lane) {
          slot_counter[found_offset] = atomicAdd(global_counter, 0);
          active = false;
        }

        warp_tile_copy<warp_size>(lane_idx, embedding_vec_size,
                                  (volatile float*)(d_values + next_idx * embedding_vec_size),
                                  (volatile float*)(vals + found_offset * embedding_vec_size));

        active_mask = warp_tile.ballot(active);
        break;
      }

      // Compare the slab data with empty key, if found empty key, mark missing task, task is
      // completed
      if (warp_tile.ballot(read_key == empty_key) != 0) {
        if (lane_idx == warp_missing_counter) {
          missing_key = next_key;
          missing_index = next_idx;
        }

        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        warp_missing_counter++;
        active_mask = warp_tile.ballot(active);
        break;
      }

      // Not found in this slab, the task is not completed, goto searching next slab
      counter++;
      next_slab = (next_slab + 1) % set_associativity;
    }

    // Unlock the slabset after operating the slabset
    warp_unlock_mutex<warp_size>(warp_tile, set_mutex[next_set]);
  }

  // After warp_tile complete the working queue, save the result for output
  // First thread of the warp_tile accumulate the missing length to global variable
  size_t warp_position;
  if (lane_idx == 0) {
    warp_position = atomicAdd(d_missing_len, (size_t)warp_missing_counter);
  }
  warp_position = warp_tile.shfl(warp_position, 0);

  if (lane_idx < warp_missing_counter) {
    d_missing_keys[warp_position + lane_idx] = missing_key;
    d_missing_index[warp_position + lane_idx] = missing_index;
  }
}
#endif

#ifdef LIBCUDACXX_VERSION
// Kernel to insert or replace the <k,v> pairs into the cache
template <typename key_type, typename slabset, typename ref_counter_type, typename mutex,
          typename atomic_ref_counter_type, typename set_hasher, typename slab_hasher,
          key_type empty_key, int set_associativity, int warp_size,
          ref_counter_type max_ref_counter_type = std::numeric_limits<ref_counter_type>::max(),
          size_t max_slab_distance = std::numeric_limits<size_t>::max()>
__global__ void insert_replace_kernel(const key_type* d_keys, const float* d_values,
                                      const size_t embedding_vec_size, const size_t len,
                                      slabset* keys, float* vals, ref_counter_type* slot_counter,
                                      mutex* set_mutex,
                                      const atomic_ref_counter_type* global_counter,
                                      const size_t capacity_in_set,
                                      const size_t task_per_warp_tile) {
  // Lane(thread) ID within a warp_tile
  cg::thread_block_tile<warp_size> warp_tile =
      cg::tiled_partition<warp_size>(cg::this_thread_block());
  const size_t lane_idx = warp_tile.thread_rank();
  // Warp tile global ID
  const size_t warp_tile_global_idx =
      (blockIdx.x * (blockDim.x / warp_size)) + warp_tile.meta_group_rank();
  // The index of key for this thread
  const size_t key_idx = (warp_tile_global_idx * task_per_warp_tile) + lane_idx;
  // The assigned key for this lane(thread)
  key_type key;
  // The dst slabset and the dst slab inside this set
  size_t src_set;
  size_t src_slab;
  // Active flag: whether current lane(thread) has unfinished task
  bool active = false;
  if (lane_idx < task_per_warp_tile) {
    if (key_idx < len) {
      active = true;
      key = d_keys[key_idx];
      src_set = set_hasher::hash(key) % capacity_in_set;
      src_slab = slab_hasher::hash(key) % set_associativity;
    }
  }

  // Lane participate in warp_tile ballot to produce warp-level work queue
  unsigned active_mask = warp_tile.ballot(active);

  // The warp-level outer loop: finish all the tasks within the work queue
  while (active_mask != 0) {
    // Next task in the work quere, start from lower index lane(thread)
    int next_lane = __ffs(active_mask) - 1;
    // Broadcast the task, the global index and the src slabset and slab to all lane in a warp_tile
    key_type next_key = warp_tile.shfl(key, next_lane);
    size_t next_idx = warp_tile.shfl(key_idx, next_lane);
    size_t next_set = warp_tile.shfl(src_set, next_lane);
    size_t next_slab = warp_tile.shfl(src_slab, next_lane);
    size_t first_slab = next_slab;

    // Counter to record how many slab have been searched
    size_t counter = 0;

    // Variable to keep the min slot counter during the probing
    ref_counter_type min_slot_counter_val = max_ref_counter_type;
    // Variable to keep the slab distance for slot with min counter
    size_t slab_distance = max_slab_distance;
    // Variable to keep the slot distance for slot with min counter within the slab
    size_t slot_distance;
    // Working queue before task started
    const unsigned old_active_mask = active_mask;

    // Lock the slabset before operating the slabset
    warp_lock_mutex<mutex, warp_size>(warp_tile, set_mutex[next_set]);

    // The warp-level inner loop: finish a single task in the work queue
    while (active_mask == old_active_mask) {
      // When all the slabs inside a slabset have been searched
      // and no empty slots or target slots are found. Replace with LRU
      if (counter >= set_associativity) {
        // (sub)Warp all-reduction, the reduction result store in all threads
        warp_min_reduction<ref_counter_type, warp_size>(warp_tile, min_slot_counter_val,
                                                        slab_distance, slot_distance);

        // Calculate the position of LR slot
        size_t target_slab = (first_slab + slab_distance) % set_associativity;
        size_t slot_index =
            (next_set * set_associativity + target_slab) * warp_size + slot_distance;

        // Replace the LR slot
        if (lane_idx == (size_t)next_lane) {
          keys[next_set].set_[target_slab].slab_[slot_distance] = key;
          slot_counter[slot_index] = global_counter->load(cuda::std::memory_order_relaxed);
        }

        warp_tile_copy<warp_size>(lane_idx, embedding_vec_size,
                                  vals + slot_index * embedding_vec_size,
                                  d_values + next_idx * embedding_vec_size);

        // Replace complete, mark this task completed
        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // The warp_tile read out the slab
      key_type read_key = keys[next_set].set_[next_slab].slab_[lane_idx];

      // Compare the slab data with the target key
      int found_lane = __ffs(warp_tile.ballot(read_key == next_key)) - 1;

      // If found target key, the insertion/replace is no longer needed.
      // Refresh the slot, the task is completed
      if (found_lane >= 0) {
        size_t found_offset = (next_set * set_associativity + next_slab) * warp_size + found_lane;
        if (lane_idx == (size_t)next_lane) {
          slot_counter[found_offset] = global_counter->load(cuda::std::memory_order_relaxed);
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // Compare the slab data with empty key.
      // If found empty key, do insertion,the task is complete
      found_lane = __ffs(warp_tile.ballot(read_key == empty_key)) - 1;
      if (found_lane >= 0) {
        size_t found_offset = (next_set * set_associativity + next_slab) * warp_size + found_lane;

        if (lane_idx == (size_t)next_lane) {
          keys[next_set].set_[next_slab].slab_[found_lane] = key;
          slot_counter[found_offset] = global_counter->load(cuda::std::memory_order_relaxed);
        }

        warp_tile_copy<warp_size>(lane_idx, embedding_vec_size,
                                  vals + found_offset * embedding_vec_size,
                                  d_values + next_idx * embedding_vec_size);

        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // If no target or unused slot found in this slab,
      // Refresh LR info, continue probing
      ref_counter_type read_slot_counter =
          slot_counter[(next_set * set_associativity + next_slab) * warp_size + lane_idx];
      if (read_slot_counter < min_slot_counter_val) {
        min_slot_counter_val = read_slot_counter;
        slab_distance = counter;
      }

      counter++;
      next_slab = (next_slab + 1) % set_associativity;
    }

    // Unlock the slabset after operating the slabset
    warp_unlock_mutex<mutex, warp_size>(warp_tile, set_mutex[next_set]);
  }
}
#else
// Kernel to insert or replace the <k,v> pairs into the cache
template <typename key_type, typename slabset, typename ref_counter_type, typename set_hasher,
          typename slab_hasher, key_type empty_key, int set_associativity, int warp_size,
          ref_counter_type max_ref_counter_type = std::numeric_limits<ref_counter_type>::max(),
          size_t max_slab_distance = std::numeric_limits<size_t>::max()>
__global__ void insert_replace_kernel(const key_type* d_keys, const float* d_values,
                                      const size_t embedding_vec_size, const size_t len,
                                      volatile slabset* keys, volatile float* vals,
                                      volatile ref_counter_type* slot_counter,
                                      volatile int* set_mutex, ref_counter_type* global_counter,
                                      const size_t capacity_in_set,
                                      const size_t task_per_warp_tile) {
  // Lane(thread) ID within a warp_tile
  cg::thread_block_tile<warp_size> warp_tile =
      cg::tiled_partition<warp_size>(cg::this_thread_block());
  const size_t lane_idx = warp_tile.thread_rank();
  // Warp tile global ID
  const size_t warp_tile_global_idx =
      (blockIdx.x * (blockDim.x / warp_size)) + warp_tile.meta_group_rank();
  // The index of key for this thread
  const size_t key_idx = (warp_tile_global_idx * task_per_warp_tile) + lane_idx;
  // The assigned key for this lane(thread)
  key_type key;
  // The dst slabset and the dst slab inside this set
  size_t src_set;
  size_t src_slab;
  // Active flag: whether current lane(thread) has unfinished task
  bool active = false;
  if (lane_idx < task_per_warp_tile) {
    if (key_idx < len) {
      active = true;
      key = d_keys[key_idx];
      src_set = set_hasher::hash(key) % capacity_in_set;
      src_slab = slab_hasher::hash(key) % set_associativity;
    }
  }

  // Lane participate in warp_tile ballot to produce warp-level work queue
  unsigned active_mask = warp_tile.ballot(active);

  // The warp-level outer loop: finish all the tasks within the work queue
  while (active_mask != 0) {
    // Next task in the work quere, start from lower index lane(thread)
    int next_lane = __ffs(active_mask) - 1;
    // Broadcast the task, the global index and the src slabset and slab to all lane in a warp_tile
    key_type next_key = warp_tile.shfl(key, next_lane);
    size_t next_idx = warp_tile.shfl(key_idx, next_lane);
    size_t next_set = warp_tile.shfl(src_set, next_lane);
    size_t next_slab = warp_tile.shfl(src_slab, next_lane);
    size_t first_slab = next_slab;

    // Counter to record how many slab have been searched
    size_t counter = 0;

    // Variable to keep the min slot counter during the probing
    ref_counter_type min_slot_counter_val = max_ref_counter_type;
    // Variable to keep the slab distance for slot with min counter
    size_t slab_distance = max_slab_distance;
    // Variable to keep the slot distance for slot with min counter within the slab
    size_t slot_distance;
    // Working queue before task started
    const unsigned old_active_mask = active_mask;

    // Lock the slabset before operating the slabset
    warp_lock_mutex<warp_size>(warp_tile, set_mutex[next_set]);

    // The warp-level inner loop: finish a single task in the work queue
    while (active_mask == old_active_mask) {
      // When all the slabs inside a slabset have been searched
      // and no empty slots or target slots are found. Replace with LRU
      if (counter >= set_associativity) {
        // (sub)Warp all-reduction, the reduction result store in all threads
        warp_min_reduction<ref_counter_type, warp_size>(warp_tile, min_slot_counter_val,
                                                        slab_distance, slot_distance);

        // Calculate the position of LR slot
        size_t target_slab = (first_slab + slab_distance) % set_associativity;
        size_t slot_index =
            (next_set * set_associativity + target_slab) * warp_size + slot_distance;

        // Replace the LR slot
        if (lane_idx == (size_t)next_lane) {
          ((volatile key_type*)(keys[next_set].set_[target_slab].slab_))[slot_distance] = key;
          slot_counter[slot_index] = atomicAdd(global_counter, 0);
        }

        warp_tile_copy<warp_size>(lane_idx, embedding_vec_size,
                                  (volatile float*)(vals + slot_index * embedding_vec_size),
                                  (volatile float*)(d_values + next_idx * embedding_vec_size));

        // Replace complete, mark this task completed
        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // The warp_tile read out the slab
      key_type read_key = ((volatile key_type*)(keys[next_set].set_[next_slab].slab_))[lane_idx];

      // Compare the slab data with the target key
      int found_lane = __ffs(warp_tile.ballot(read_key == next_key)) - 1;

      // If found target key, the insertion/replace is no longer needed.
      // Refresh the slot, the task is completed
      if (found_lane >= 0) {
        size_t found_offset = (next_set * set_associativity + next_slab) * warp_size + found_lane;
        if (lane_idx == (size_t)next_lane) {
          slot_counter[found_offset] = atomicAdd(global_counter, 0);
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // Compare the slab data with empty key.
      // If found empty key, do insertion,the task is complete
      found_lane = __ffs(warp_tile.ballot(read_key == empty_key)) - 1;
      if (found_lane >= 0) {
        size_t found_offset = (next_set * set_associativity + next_slab) * warp_size + found_lane;

        if (lane_idx == (size_t)next_lane) {
          ((volatile key_type*)(keys[next_set].set_[next_slab].slab_))[found_lane] = key;
          slot_counter[found_offset] = atomicAdd(global_counter, 0);
        }

        warp_tile_copy<warp_size>(lane_idx, embedding_vec_size,
                                  (volatile float*)(vals + found_offset * embedding_vec_size),
                                  (volatile float*)(d_values + next_idx * embedding_vec_size));

        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // If no target or unused slot found in this slab,
      // Refresh LR info, continue probing
      ref_counter_type read_slot_counter =
          slot_counter[(next_set * set_associativity + next_slab) * warp_size + lane_idx];
      if (read_slot_counter < min_slot_counter_val) {
        min_slot_counter_val = read_slot_counter;
        slab_distance = counter;
      }

      counter++;
      next_slab = (next_slab + 1) % set_associativity;
    }

    // Unlock the slabset after operating the slabset
    warp_unlock_mutex<warp_size>(warp_tile, set_mutex[next_set]);
  }
}
#endif

#ifdef LIBCUDACXX_VERSION
// Kernel to update the existing keys in the cache
// Will not change the locality information
template <typename key_type, typename slabset, typename set_hasher, typename slab_hasher,
          typename mutex, key_type empty_key, int set_associativity, int warp_size>
__global__ void update_kernel(const key_type* d_keys, const size_t len, const float* d_values,
                              const size_t embedding_vec_size, const size_t capacity_in_set,
                              const slabset* keys, float* vals, mutex* set_mutex,
                              const size_t task_per_warp_tile) {
  // Lane(thread) ID within a warp_tile
  cg::thread_block_tile<warp_size> warp_tile =
      cg::tiled_partition<warp_size>(cg::this_thread_block());
  const size_t lane_idx = warp_tile.thread_rank();
  // Warp tile global ID
  const size_t warp_tile_global_idx =
      (blockIdx.x * (blockDim.x / warp_size)) + warp_tile.meta_group_rank();
  // The index of key for this thread
  const size_t key_idx = (warp_tile_global_idx * task_per_warp_tile) + lane_idx;
  // The assigned key for this lane(thread)
  key_type key;
  // The dst slabset and the dst slab inside this set
  size_t src_set;
  size_t src_slab;
  // Active flag: whether current lane(thread) has unfinished task
  bool active = false;
  if (lane_idx < task_per_warp_tile) {
    if (key_idx < len) {
      active = true;
      key = d_keys[key_idx];
      src_set = set_hasher::hash(key) % capacity_in_set;
      src_slab = slab_hasher::hash(key) % set_associativity;
    }
  }

  // Lane participate in warp_tile ballot to produce warp-level work queue
  unsigned active_mask = warp_tile.ballot(active);

  // The warp-level outer loop: finish all the tasks within the work queue
  while (active_mask != 0) {
    // Next task in the work quere, start from lower index lane(thread)
    int next_lane = __ffs(active_mask) - 1;
    // Broadcast the task and the global index to all lane in the warp_tile
    key_type next_key = warp_tile.shfl(key, next_lane);
    size_t next_idx = warp_tile.shfl(key_idx, next_lane);
    size_t next_set = warp_tile.shfl(src_set, next_lane);
    size_t next_slab = warp_tile.shfl(src_slab, next_lane);

    // Counter to record how many slab have been searched
    size_t counter = 0;

    // Working queue before task started
    const unsigned old_active_mask = active_mask;

    // Lock the slabset before operating the slabset
    warp_lock_mutex<mutex, warp_size>(warp_tile, set_mutex[next_set]);

    // The warp-level inner loop: finish a single task in the work queue
    while (active_mask == old_active_mask) {
      // When all the slabs inside a slabset have been searched, mark missing task, do nothing, task
      // complete
      if (counter >= set_associativity) {
        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // The warp_tile read out the slab
      key_type read_key = keys[next_set].set_[next_slab].slab_[lane_idx];

      // Compare the slab data with the target key
      int found_lane = __ffs(warp_tile.ballot(read_key == next_key)) - 1;

      // If found, mark hit task, update the value, the task is completed
      if (found_lane >= 0) {
        size_t found_offset = (next_set * set_associativity + next_slab) * warp_size + found_lane;
        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        warp_tile_copy<warp_size>(lane_idx, embedding_vec_size,
                                  vals + found_offset * embedding_vec_size,
                                  d_values + next_idx * embedding_vec_size);

        active_mask = warp_tile.ballot(active);
        break;
      }

      // Compare the slab data with empty key, if found empty key, mark missing task, do nothing,
      // task is completed
      if (warp_tile.ballot(read_key == empty_key) != 0) {
        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // Not found in this slab, the task is not completed, goto searching next slab
      counter++;
      next_slab = (next_slab + 1) % set_associativity;
    }

    // Unlock the slabset after operating the slabset
    warp_unlock_mutex<mutex, warp_size>(warp_tile, set_mutex[next_set]);
  }
}
#else
// Kernel to update the existing keys in the cache
// Will not change the locality information
template <typename key_type, typename slabset, typename set_hasher, typename slab_hasher,
          key_type empty_key, int set_associativity, int warp_size>
__global__ void update_kernel(const key_type* d_keys, const size_t len, const float* d_values,
                              const size_t embedding_vec_size, const size_t capacity_in_set,
                              volatile slabset* keys, volatile float* vals, volatile int* set_mutex,
                              const size_t task_per_warp_tile) {
  // Lane(thread) ID within a warp_tile
  cg::thread_block_tile<warp_size> warp_tile =
      cg::tiled_partition<warp_size>(cg::this_thread_block());
  const size_t lane_idx = warp_tile.thread_rank();
  // Warp tile global ID
  const size_t warp_tile_global_idx =
      (blockIdx.x * (blockDim.x / warp_size)) + warp_tile.meta_group_rank();
  // The index of key for this thread
  const size_t key_idx = (warp_tile_global_idx * task_per_warp_tile) + lane_idx;
  // The assigned key for this lane(thread)
  key_type key;
  // The dst slabset and the dst slab inside this set
  size_t src_set;
  size_t src_slab;
  // Active flag: whether current lane(thread) has unfinished task
  bool active = false;
  if (lane_idx < task_per_warp_tile) {
    if (key_idx < len) {
      active = true;
      key = d_keys[key_idx];
      src_set = set_hasher::hash(key) % capacity_in_set;
      src_slab = slab_hasher::hash(key) % set_associativity;
    }
  }

  // Lane participate in warp_tile ballot to produce warp-level work queue
  unsigned active_mask = warp_tile.ballot(active);

  // The warp-level outer loop: finish all the tasks within the work queue
  while (active_mask != 0) {
    // Next task in the work quere, start from lower index lane(thread)
    int next_lane = __ffs(active_mask) - 1;
    // Broadcast the task and the global index to all lane in the warp_tile
    key_type next_key = warp_tile.shfl(key, next_lane);
    size_t next_idx = warp_tile.shfl(key_idx, next_lane);
    size_t next_set = warp_tile.shfl(src_set, next_lane);
    size_t next_slab = warp_tile.shfl(src_slab, next_lane);

    // Counter to record how many slab have been searched
    size_t counter = 0;

    // Working queue before task started
    const unsigned old_active_mask = active_mask;

    // Lock the slabset before operating the slabset
    warp_lock_mutex<warp_size>(warp_tile, set_mutex[next_set]);

    // The warp-level inner loop: finish a single task in the work queue
    while (active_mask == old_active_mask) {
      // When all the slabs inside a slabset have been searched, mark missing task, do nothing, task
      // complete
      if (counter >= set_associativity) {
        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // The warp_tile read out the slab
      key_type read_key = ((volatile key_type*)(keys[next_set].set_[next_slab].slab_))[lane_idx];

      // Compare the slab data with the target key
      int found_lane = __ffs(warp_tile.ballot(read_key == next_key)) - 1;

      // If found, mark hit task, update the value, the task is completed
      if (found_lane >= 0) {
        size_t found_offset = (next_set * set_associativity + next_slab) * warp_size + found_lane;
        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        warp_tile_copy<warp_size>(lane_idx, embedding_vec_size,
                                  (volatile float*)(vals + found_offset * embedding_vec_size),
                                  (volatile float*)(d_values + next_idx * embedding_vec_size));

        active_mask = warp_tile.ballot(active);
        break;
      }

      // Compare the slab data with empty key, if found empty key, mark missing task, do nothing,
      // task is completed
      if (warp_tile.ballot(read_key == empty_key) != 0) {
        if (lane_idx == (size_t)next_lane) {
          active = false;
        }

        active_mask = warp_tile.ballot(active);
        break;
      }

      // Not found in this slab, the task is not completed, goto searching next slab
      counter++;
      next_slab = (next_slab + 1) % set_associativity;
    }

    // Unlock the slabset after operating the slabset
    warp_unlock_mutex<warp_size>(warp_tile, set_mutex[next_set]);
  }
}
#endif

#ifdef LIBCUDACXX_VERSION
template <typename key_type, typename slabset, typename mutex, key_type empty_key,
          int set_associativity, int warp_size>
__global__ void dump_kernel(key_type* d_keys, size_t* d_dump_counter, const slabset* keys,
                            mutex* set_mutex, const size_t start_set_index,
                            const size_t end_set_index) {
  // Block-level counter used by all warp tiles within a block
  __shared__ uint32_t block_acc;
  // Initialize block-level counter
  if (threadIdx.x == 0) {
    block_acc = 0;
  }
  __syncthreads();
  // Lane(thread) ID within a warp tile
  cg::thread_block_tile<warp_size> warp_tile =
      cg::tiled_partition<warp_size>(cg::this_thread_block());
  const size_t lane_idx = warp_tile.thread_rank();
  // Warp tile target slabset id
  const size_t set_idx =
      ((blockIdx.x * (blockDim.x / warp_size)) + warp_tile.meta_group_rank()) + start_set_index;
  // Keys dump from cache
  key_type read_key[set_associativity];
  // Lane(thread) offset for storing each key
  uint32_t thread_key_offset[set_associativity];
  // Warp offset for storing each key
  uint32_t warp_key_offset;
  // Block offset for storing each key
  __shared__ size_t block_key_offset;

  // Warp tile dump target slabset
  if (set_idx < end_set_index) {
    // Lock the slabset before operating the slabset
    warp_lock_mutex<mutex, warp_size>(warp_tile, set_mutex[set_idx]);

    // The warp tile read out the slabset
    for (unsigned slab_id = 0; slab_id < set_associativity; slab_id++) {
      // The warp tile read out a slab
      read_key[slab_id] = keys[set_idx].set_[slab_id].slab_[lane_idx];
    }

    // Finish dumping the slabset, unlock the slabset
    warp_unlock_mutex<mutex, warp_size>(warp_tile, set_mutex[set_idx]);

    // Each lane(thread) within the warp tile calculate the offset to store its keys
    uint32_t warp_tile_total_keys = 0;
    for (unsigned slab_id = 0; slab_id < set_associativity; slab_id++) {
      unsigned valid_mask = warp_tile.ballot(read_key[slab_id] != empty_key);
      thread_key_offset[slab_id] =
          __popc(valid_mask & ((1U << lane_idx) - 1U)) + warp_tile_total_keys;
      warp_tile_total_keys = warp_tile_total_keys + __popc(valid_mask);
    }

    // Each warp tile request a unique place from the block-level counter
    if (lane_idx == 0) {
      warp_key_offset = atomicAdd(&block_acc, warp_tile_total_keys);
    }
    warp_key_offset = warp_tile.shfl(warp_key_offset, 0);
  }

  // Each block request a unique place in global memory output buffer
  __syncthreads();
  if (threadIdx.x == 0) {
    block_key_offset = atomicAdd(d_dump_counter, (size_t)block_acc);
  }
  __syncthreads();

  // Warp tile store the (non-empty)keys back to output buffer
  if (set_idx < end_set_index) {
    for (unsigned slab_id = 0; slab_id < set_associativity; slab_id++) {
      if (read_key[slab_id] != empty_key) {
        d_keys[block_key_offset + warp_key_offset + thread_key_offset[slab_id]] = read_key[slab_id];
      }
    }
  }
}
#else
template <typename key_type, typename slabset, key_type empty_key, int set_associativity,
          int warp_size>
__global__ void dump_kernel(key_type* d_keys, size_t* d_dump_counter, volatile slabset* keys,
                            volatile int* set_mutex, const size_t start_set_index,
                            const size_t end_set_index) {
  // Block-level counter used by all warp tiles within a block
  __shared__ uint32_t block_acc;
  // Initialize block-level counter
  if (threadIdx.x == 0) {
    block_acc = 0;
  }
  __syncthreads();
  // Lane(thread) ID within a warp tile
  cg::thread_block_tile<warp_size> warp_tile =
      cg::tiled_partition<warp_size>(cg::this_thread_block());
  const size_t lane_idx = warp_tile.thread_rank();
  // Warp tile target slabset id
  const size_t set_idx =
      ((blockIdx.x * (blockDim.x / warp_size)) + warp_tile.meta_group_rank()) + start_set_index;
  // Keys dump from cache
  key_type read_key[set_associativity];
  // Lane(thread) offset for storing each key
  uint32_t thread_key_offset[set_associativity];
  // Warp offset for storing each key
  uint32_t warp_key_offset;
  // Block offset for storing each key
  __shared__ size_t block_key_offset;

  // Warp tile dump target slabset
  if (set_idx < end_set_index) {
    // Lock the slabset before operating the slabset
    warp_lock_mutex<warp_size>(warp_tile, set_mutex[set_idx]);

    // The warp tile read out the slabset
    for (unsigned slab_id = 0; slab_id < set_associativity; slab_id++) {
      // The warp tile read out a slab
      read_key[slab_id] = ((volatile key_type*)(keys[set_idx].set_[slab_id].slab_))[lane_idx];
    }

    // Finish dumping the slabset, unlock the slabset
    warp_unlock_mutex<warp_size>(warp_tile, set_mutex[set_idx]);

    // Each lane(thread) within the warp tile calculate the offset to store its keys
    uint32_t warp_tile_total_keys = 0;
    for (unsigned slab_id = 0; slab_id < set_associativity; slab_id++) {
      unsigned valid_mask = warp_tile.ballot(read_key[slab_id] != empty_key);
      thread_key_offset[slab_id] =
          __popc(valid_mask & ((1U << lane_idx) - 1U)) + warp_tile_total_keys;
      warp_tile_total_keys = warp_tile_total_keys + __popc(valid_mask);
    }

    // Each warp tile request a unique place from the block-level counter
    if (lane_idx == 0) {
      warp_key_offset = atomicAdd(&block_acc, warp_tile_total_keys);
    }
    warp_key_offset = warp_tile.shfl(warp_key_offset, 0);
  }

  // Each block request a unique place in global memory output buffer
  __syncthreads();
  if (threadIdx.x == 0) {
    block_key_offset = atomicAdd(d_dump_counter, (size_t)block_acc);
  }
  __syncthreads();

  // Warp tile store the (non-empty)keys back to output buffer
  if (set_idx < end_set_index) {
    for (unsigned slab_id = 0; slab_id < set_associativity; slab_id++) {
      if (read_key[slab_id] != empty_key) {
        d_keys[block_key_offset + warp_key_offset + thread_key_offset[slab_id]] = read_key[slab_id];
      }
    }
  }
}
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////

#ifdef LIBCUDACXX_VERSION
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
          slab_hasher>::gpu_cache(const size_t capacity_in_set, const size_t embedding_vec_size)
    : capacity_in_set_(capacity_in_set), embedding_vec_size_(embedding_vec_size) {
  // Check parameter
  if (capacity_in_set_ == 0) {
    printf("Error: Invalid value for capacity_in_set.\n");
    return;
  }
  if (embedding_vec_size_ == 0) {
    printf("Error: Invalid value for embedding_vec_size.\n");
    return;
  }
  if (set_associativity <= 0) {
    printf("Error: Invalid value for set_associativity.\n");
    return;
  }
  if (warp_size != 1 && warp_size != 2 && warp_size != 4 && warp_size != 8 && warp_size != 16 &&
      warp_size != 32) {
    printf("Error: Invalid value for warp_size.\n");
    return;
  }

  // Get the current CUDA dev
  CUDA_CHECK(cudaGetDevice(&dev_));

  // Calculate # of slot
  num_slot_ = capacity_in_set_ * set_associativity * warp_size;

  // Allocate GPU memory for cache
  CUDA_CHECK(cudaMalloc((void**)&keys_, sizeof(slabset) * capacity_in_set_));
  CUDA_CHECK(cudaMalloc((void**)&vals_, sizeof(float) * embedding_vec_size_ * num_slot_));
  CUDA_CHECK(cudaMalloc((void**)&slot_counter_, sizeof(ref_counter_type) * num_slot_));
  CUDA_CHECK(cudaMalloc((void**)&global_counter_, sizeof(atomic_ref_counter_type)));

  // Allocate GPU memory for set mutex
  CUDA_CHECK(cudaMalloc((void**)&set_mutex_, sizeof(mutex) * capacity_in_set_));

  // Initialize the cache, set all entry to unused <K,V>
  init_cache<<<((num_slot_ - 1) / BLOCK_SIZE_) + 1, BLOCK_SIZE_>>>(
      keys_, slot_counter_, global_counter_, num_slot_, empty_key, set_mutex_, capacity_in_set_);

  // Wait for initialization to finish
  CUDA_CHECK(cudaStreamSynchronize(0));
  CUDA_CHECK(cudaGetLastError());
}
#else
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
          slab_hasher>::gpu_cache(const size_t capacity_in_set, const size_t embedding_vec_size)
    : capacity_in_set_(capacity_in_set), embedding_vec_size_(embedding_vec_size) {
  // Check parameter
  if (capacity_in_set_ == 0) {
    printf("Error: Invalid value for capacity_in_set.\n");
    return;
  }
  if (embedding_vec_size_ == 0) {
    printf("Error: Invalid value for embedding_vec_size.\n");
    return;
  }
  if (set_associativity <= 0) {
    printf("Error: Invalid value for set_associativity.\n");
    return;
  }
  if (warp_size != 1 && warp_size != 2 && warp_size != 4 && warp_size != 8 && warp_size != 16 &&
      warp_size != 32) {
    printf("Error: Invalid value for warp_size.\n");
    return;
  }

  // Get the current CUDA dev
  CUDA_CHECK(cudaGetDevice(&dev_));

  // Calculate # of slot
  num_slot_ = capacity_in_set_ * set_associativity * warp_size;

  // Allocate GPU memory for cache
  CUDA_CHECK(cudaMalloc((void**)&keys_, sizeof(slabset) * capacity_in_set_));
  CUDA_CHECK(cudaMalloc((void**)&vals_, sizeof(float) * embedding_vec_size_ * num_slot_));
  CUDA_CHECK(cudaMalloc((void**)&slot_counter_, sizeof(ref_counter_type) * num_slot_));
  CUDA_CHECK(cudaMalloc((void**)&global_counter_, sizeof(ref_counter_type)));

  // Allocate GPU memory for set mutex
  CUDA_CHECK(cudaMalloc((void**)&set_mutex_, sizeof(int) * capacity_in_set_));

  // Initialize the cache, set all entry to unused <K,V>
  init_cache<<<((num_slot_ - 1) / BLOCK_SIZE_) + 1, BLOCK_SIZE_>>>(
      keys_, slot_counter_, global_counter_, num_slot_, empty_key, set_mutex_, capacity_in_set_);

  // Wait for initialization to finish
  CUDA_CHECK(cudaStreamSynchronize(0));
  CUDA_CHECK(cudaGetLastError());
}
#endif

#ifdef LIBCUDACXX_VERSION
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
          slab_hasher>::~gpu_cache() {
  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;

  // Check device
  dev_restorer.check_device(dev_);

  // Destruct CUDA std object
  destruct_kernel<<<((capacity_in_set_ - 1) / BLOCK_SIZE_) + 1, BLOCK_SIZE_>>>(
      global_counter_, set_mutex_, capacity_in_set_);
  // Wait for destruction to finish
  CUDA_CHECK(cudaStreamSynchronize(0));

  // Free GPU memory for cache
  CUDA_CHECK(cudaFree(keys_));
  CUDA_CHECK(cudaFree(vals_));
  CUDA_CHECK(cudaFree(slot_counter_));
  CUDA_CHECK(cudaFree(global_counter_));
  // Free GPU memory for set mutex
  CUDA_CHECK(cudaFree(set_mutex_));
}
#else
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
          slab_hasher>::~gpu_cache() noexcept(false) {
  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;

  // Check device
  dev_restorer.check_device(dev_);

  // Free GPU memory for cache
  CUDA_CHECK(cudaFree(keys_));
  CUDA_CHECK(cudaFree(vals_));
  CUDA_CHECK(cudaFree(slot_counter_));
  CUDA_CHECK(cudaFree(global_counter_));
  // Free GPU memory for set mutex
  CUDA_CHECK(cudaFree(set_mutex_));
}
#endif

#ifdef LIBCUDACXX_VERSION
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
void gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
               slab_hasher>::Query(const key_type* d_keys, const size_t len, float* d_values,
                                   uint64_t* d_missing_index, key_type* d_missing_keys,
                                   size_t* d_missing_len, cudaStream_t stream,
                                   const size_t task_per_warp_tile) {
  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;
  // Check device
  dev_restorer.check_device(dev_);

  // Check if it is a valid query
  if (len == 0) {
    // Set the d_missing_len to 0 before return
    CUDA_CHECK(cudaMemsetAsync(d_missing_len, 0, sizeof(size_t), stream));
    return;
  }

  // Update the global counter as user perform a new(most recent) read operation to the cache
  // Resolve distance overflow issue as well.
  update_kernel_overflow_ignore<atomic_ref_counter_type>
      <<<1, 1, 0, stream>>>(global_counter_, d_missing_len);

  // Read from the cache
  // Touch and refresh the hitting slot
  const size_t keys_per_block = (BLOCK_SIZE_ / warp_size) * task_per_warp_tile;
  const size_t grid_size = ((len - 1) / keys_per_block) + 1;
  get_kernel<key_type, ref_counter_type, atomic_ref_counter_type, slabset, set_hasher, slab_hasher,
             mutex, empty_key, set_associativity, warp_size><<<grid_size, BLOCK_SIZE_, 0, stream>>>(
      d_keys, len, d_values, embedding_vec_size_, d_missing_index, d_missing_keys, d_missing_len,
      global_counter_, slot_counter_, capacity_in_set_, keys_, vals_, set_mutex_,
      task_per_warp_tile);

  // Check for GPU error before return
  CUDA_CHECK(cudaGetLastError());
}
#else
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
void gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
               slab_hasher>::Query(const key_type* d_keys, const size_t len, float* d_values,
                                   uint64_t* d_missing_index, key_type* d_missing_keys,
                                   size_t* d_missing_len, cudaStream_t stream,
                                   const size_t task_per_warp_tile) {
  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;
  // Check device
  dev_restorer.check_device(dev_);

  // Check if it is a valid query
  if (len == 0) {
    // Set the d_missing_len to 0 before return
    CUDA_CHECK(cudaMemsetAsync(d_missing_len, 0, sizeof(size_t), stream));
    return;
  }

  // Update the global counter as user perform a new(most recent) read operation to the cache
  // Resolve distance overflow issue as well.
  update_kernel_overflow_ignore<ref_counter_type>
      <<<1, 1, 0, stream>>>(global_counter_, d_missing_len);

  // Read from the cache
  // Touch and refresh the hitting slot
  const size_t keys_per_block = (BLOCK_SIZE_ / warp_size) * task_per_warp_tile;
  const size_t grid_size = ((len - 1) / keys_per_block) + 1;
  get_kernel<key_type, ref_counter_type, slabset, set_hasher, slab_hasher, empty_key,
             set_associativity, warp_size><<<grid_size, BLOCK_SIZE_, 0, stream>>>(
      d_keys, len, d_values, embedding_vec_size_, d_missing_index, d_missing_keys, d_missing_len,
      global_counter_, slot_counter_, capacity_in_set_, keys_, vals_, set_mutex_,
      task_per_warp_tile);

  // Check for GPU error before return
  CUDA_CHECK(cudaGetLastError());
}
#endif

#ifdef LIBCUDACXX_VERSION
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
void gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
               slab_hasher>::Replace(const key_type* d_keys, const size_t len,
                                     const float* d_values, cudaStream_t stream,
                                     const size_t task_per_warp_tile) {
  // Check if it is a valid replacement
  if (len == 0) {
    return;
  }

  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;
  // Check device
  dev_restorer.check_device(dev_);

  // Try to insert the <k,v> paris into the cache as long as there are unused slot
  // Then replace the <k,v> pairs into the cache
  const size_t keys_per_block = (BLOCK_SIZE_ / warp_size) * task_per_warp_tile;
  const size_t grid_size = ((len - 1) / keys_per_block) + 1;
  insert_replace_kernel<key_type, slabset, ref_counter_type, mutex, atomic_ref_counter_type,
                        set_hasher, slab_hasher, empty_key, set_associativity, warp_size>
      <<<grid_size, BLOCK_SIZE_, 0, stream>>>(d_keys, d_values, embedding_vec_size_, len, keys_,
                                              vals_, slot_counter_, set_mutex_, global_counter_,
                                              capacity_in_set_, task_per_warp_tile);

  // Check for GPU error before return
  CUDA_CHECK(cudaGetLastError());
}
#else
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
void gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
               slab_hasher>::Replace(const key_type* d_keys, const size_t len,
                                     const float* d_values, cudaStream_t stream,
                                     const size_t task_per_warp_tile) {
  // Check if it is a valid replacement
  if (len == 0) {
    return;
  }

  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;
  // Check device
  dev_restorer.check_device(dev_);

  // Try to insert the <k,v> paris into the cache as long as there are unused slot
  // Then replace the <k,v> pairs into the cache
  const size_t keys_per_block = (BLOCK_SIZE_ / warp_size) * task_per_warp_tile;
  const size_t grid_size = ((len - 1) / keys_per_block) + 1;
  insert_replace_kernel<key_type, slabset, ref_counter_type, set_hasher, slab_hasher, empty_key,
                        set_associativity, warp_size><<<grid_size, BLOCK_SIZE_, 0, stream>>>(
      d_keys, d_values, embedding_vec_size_, len, keys_, vals_, slot_counter_, set_mutex_,
      global_counter_, capacity_in_set_, task_per_warp_tile);

  // Check for GPU error before return
  CUDA_CHECK(cudaGetLastError());
}
#endif

#ifdef LIBCUDACXX_VERSION
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
void gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
               slab_hasher>::Update(const key_type* d_keys, const size_t len, const float* d_values,
                                    cudaStream_t stream, const size_t task_per_warp_tile) {
  // Check if it is a valid update request
  if (len == 0) {
    return;
  }

  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;
  // Check device
  dev_restorer.check_device(dev_);

  // Update the value of input keys that are existed in the cache
  const size_t keys_per_block = (BLOCK_SIZE_ / warp_size) * task_per_warp_tile;
  const size_t grid_size = ((len - 1) / keys_per_block) + 1;
  update_kernel<key_type, slabset, set_hasher, slab_hasher, mutex, empty_key, set_associativity,
                warp_size><<<grid_size, BLOCK_SIZE_, 0, stream>>>(
      d_keys, len, d_values, embedding_vec_size_, capacity_in_set_, keys_, vals_, set_mutex_,
      task_per_warp_tile);

  // Check for GPU error before return
  CUDA_CHECK(cudaGetLastError());
}
#else
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
void gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
               slab_hasher>::Update(const key_type* d_keys, const size_t len, const float* d_values,
                                    cudaStream_t stream, const size_t task_per_warp_tile) {
  // Check if it is a valid update request
  if (len == 0) {
    return;
  }

  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;
  // Check device
  dev_restorer.check_device(dev_);

  // Update the value of input keys that are existed in the cache
  const size_t keys_per_block = (BLOCK_SIZE_ / warp_size) * task_per_warp_tile;
  const size_t grid_size = ((len - 1) / keys_per_block) + 1;
  update_kernel<key_type, slabset, set_hasher, slab_hasher, empty_key, set_associativity, warp_size>
      <<<grid_size, BLOCK_SIZE_, 0, stream>>>(d_keys, len, d_values, embedding_vec_size_,
                                              capacity_in_set_, keys_, vals_, set_mutex_,
                                              task_per_warp_tile);

  // Check for GPU error before return
  CUDA_CHECK(cudaGetLastError());
}
#endif

#ifdef LIBCUDACXX_VERSION
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
void gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
               slab_hasher>::Dump(key_type* d_keys, size_t* d_dump_counter,
                                  const size_t start_set_index, const size_t end_set_index,
                                  cudaStream_t stream) {
  // Check if it is a valid dump request
  if (start_set_index >= capacity_in_set_) {
    printf("Error: Invalid value for start_set_index. Nothing dumped.\n");
    return;
  }
  if (end_set_index <= start_set_index || end_set_index > capacity_in_set_) {
    printf("Error: Invalid value for end_set_index. Nothing dumped.\n");
    return;
  }

  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;
  // Check device
  dev_restorer.check_device(dev_);

  // Set the global counter to 0 first
  CUDA_CHECK(cudaMemsetAsync(d_dump_counter, 0, sizeof(size_t), stream));

  // Dump keys from the cache
  const size_t grid_size =
      (((end_set_index - start_set_index) - 1) / (BLOCK_SIZE_ / warp_size)) + 1;
  dump_kernel<key_type, slabset, mutex, empty_key, set_associativity, warp_size>
      <<<grid_size, BLOCK_SIZE_, 0, stream>>>(d_keys, d_dump_counter, keys_, set_mutex_,
                                              start_set_index, end_set_index);

  // Check for GPU error before return
  CUDA_CHECK(cudaGetLastError());
}
#else
template <typename key_type, typename ref_counter_type, key_type empty_key, int set_associativity,
          int warp_size, typename set_hasher, typename slab_hasher>
void gpu_cache<key_type, ref_counter_type, empty_key, set_associativity, warp_size, set_hasher,
               slab_hasher>::Dump(key_type* d_keys, size_t* d_dump_counter,
                                  const size_t start_set_index, const size_t end_set_index,
                                  cudaStream_t stream) {
  // Check if it is a valid dump request
  if (start_set_index >= capacity_in_set_) {
    printf("Error: Invalid value for start_set_index. Nothing dumped.\n");
    return;
  }
  if (end_set_index <= start_set_index || end_set_index > capacity_in_set_) {
    printf("Error: Invalid value for end_set_index. Nothing dumped.\n");
    return;
  }

  // Device Restorer
  nv::CudaDeviceRestorer dev_restorer;
  // Check device
  dev_restorer.check_device(dev_);

  // Set the global counter to 0 first
  CUDA_CHECK(cudaMemsetAsync(d_dump_counter, 0, sizeof(size_t), stream));

  // Dump keys from the cache
  const size_t grid_size =
      (((end_set_index - start_set_index) - 1) / (BLOCK_SIZE_ / warp_size)) + 1;
  dump_kernel<key_type, slabset, empty_key, set_associativity, warp_size>
      <<<grid_size, BLOCK_SIZE_, 0, stream>>>(d_keys, d_dump_counter, keys_, set_mutex_,
                                              start_set_index, end_set_index);

  // Check for GPU error before return
  CUDA_CHECK(cudaGetLastError());
}
#endif

template class gpu_cache<unsigned int, uint64_t, std::numeric_limits<unsigned int>::max(),
                         SET_ASSOCIATIVITY, SLAB_SIZE>;
template class gpu_cache<long long, uint64_t, std::numeric_limits<long long>::max(),
                         SET_ASSOCIATIVITY, SLAB_SIZE>;
}  // namespace gpu_cache
