Dateien
rocm-systems/internal/clients/spts/spts_kernel.h
T
2024-07-01 09:57:08 -05:00

2108 Zeilen
92 KiB
C++

/********************************************************************************
* Copyright (c) 2024 Advanced Micro Devices, Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
********************************************************************************/
#include "GPUHelper.h"
#include <hip/hip_runtime.h>
#include <hip/math_functions.h>
#include <hip/device_functions.h>
#ifdef USE_ROC_SHMEM
#include "roc_shmem.hpp"
using namespace rocshmem;
#endif
#ifndef WF_PER_WG
#error "WF_PER_WG undefined!"
#endif
#ifndef WF_SIZE
#error "WF_SIZE undefind!"
#endif
#define as_uint (unsigned int)
#define as_ulong (unsigned long long)
#define as_float (float)
#ifdef USE_DOUBLE
typedef double FPTYPE;
#else
typedef float FPTYPE;
#endif
// GCN3 and below require slightly different inline asm than Vega
// v_add_u32 requires a "vcc" register output modifier on GCN3, but not on Vega
// global_load_ in Vega is required to be flat_load_ in GCN3 and below.
// Same for global_store_ and flat_store_.
// However, the global_ instructions require an "off" modifier.
#if defined(GCN3) || defined(GCN2)
#define VCC "vcc"
#define MEM_PREFIX "flat"
#define OFF_MODIFIER ""
#else
#define VCC ""
#define MEM_PREFIX "global"
#define OFF_MODIFIER "off"
#endif
#ifndef GCN2
#define LGKMCNT_0 0xc07f // GCN3 added more VMCNT bits at the upper end of the SIMM16
#define WAKEUP "s_wakeup"
#else
#define LGKMCNT_0 0x7f
#define WAKEUP "" // s_wakeup not supported on old GPUs
#endif
#define __builtin_amdgcn_ds_bpermute __hip_ds_bpermute
#define __builtin_amdgcn_ds_swizzle __hip_ds_swizzle
#define __builtin_amdgcn_mov_dpp __hip_move_dpp
#define HIP_ENABLE_PRINTF
// Internal functions to wrap atomics, depending on if we support 64-bit
// atomics or not. Helps keep the code clean in the other parts of the code.
// All of the 32-bit atomics are built assuming we're on a little endian architecture.
__device__
inline unsigned long spts_atomic_cmpxchg(unsigned long long *const ptr,
const unsigned long long compare,
const unsigned long long val)
{
#ifdef USE_DOUBLE
return atomicCAS(ptr, compare, val);
#else
return atomicCAS(ptr, compare, val);
#endif
}
__device__
void atomic_set (FPTYPE *ptr, FPTYPE temp)
{
#ifdef USE_DOUBLE
unsigned long long newVal;
unsigned long long prevVal;
do
{
prevVal = as_ulong(*ptr);
newVal = as_ulong(temp);
} while (spts_atomic_cmpxchg((unsigned long long *)ptr, prevVal, newVal) != prevVal);
#else
unsigned long long newVal;
unsigned long long prevVal;
do
{
prevVal = as_uint(*ptr);
newVal = as_uint(temp);
} while (spts_atomic_cmpxchg((unsigned long long *)ptr, prevVal, newVal) != prevVal);
#endif
}
__device__
inline void atomic_set_done(uint * done_array, uint row, uint val_to_set)
{
atomicOr(&(done_array[row]), val_to_set);
}
__device__
inline unsigned int atomic_get_done(uint * done_array, uint val_to_check)
{
return atomicOr(&(done_array[val_to_check]), 0x0);
}
// Use a traditional LDS-based reduction to have all of the threads in the wave
// add their values into OUTPUT_THREAD's variable.
__device__
FPTYPE lds_reduction(FPTYPE temp_sum, __shared__ FPTYPE *lds,
unsigned int start_of_this_row, unsigned int end_of_this_row,
unsigned int wg_lid)
{
const unsigned int lid = wg_lid % WF_SIZE;
// Have all the threads in a workgroup reduce their data into a single
// value that's then read by the lead thread
// We start by calculating how many layers of reduction we actually need.
// If this is a very short row (smaller than our wavefront size), then we don't need
// to do all iterations of the below loop.
unsigned int num_items = min(end_of_this_row - start_of_this_row - 1, (uint)WF_SIZE);
// find next highest power of two. So if we have 5 things to reduce, we need to
// do a reduction from 8 threads' values. The last 3 will be '0'
num_items = 1 << (CHAR_BIT*(sizeof(unsigned int))-__clz(num_items-1));
for (int i = num_items >> 1; i > 0; i >>= 1)
{
lds[wg_lid] = temp_sum;
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
if (lid < i)
temp_sum += lds[wg_lid + i];
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
}
// at this point, thread 0's "temp_sum" contains the final useful value.
return temp_sum;
}
// Use a traditional LDS-based reduction to have all of the threads in the wave
// add their values into OUTPUT_THREAD's variable.
// It hides the max work behind the same s_waitcnt on local memory,
// so it should be faster than calling the reduce function twice in a row.
__device__
FPTYPE lds_reduction_two(FPTYPE temp_sum, unsigned int row_max_depth,
__shared__ FPTYPE *lds, __shared__ unsigned int *max_depth,
unsigned int start_of_this_row, unsigned int end_of_this_row,
unsigned int wg_lid)
{
const unsigned int lid = wg_lid % WF_SIZE;
// Have all the threads in a workgroup reduce their data into a single
// value that's then read by the lead thread
// We start by calculating how many layers of reduction we actually need.
// If this is a very short row (smaller than our wavefront size), then we don't need
// to do all iterations of the below loop.
unsigned int num_items = min(end_of_this_row - start_of_this_row - 1, (uint)WF_SIZE);
// find next highest power of two. So if we have 5 things to reduce, we need to
// do a reduction from 8 threads' values. The last 3 will be '0'
num_items = 1 << (CHAR_BIT*(sizeof(unsigned int))-__clz(num_items-1));
for (int i = num_items >> 1; i > 0; i >>= 1)
{
lds[wg_lid] = temp_sum;
max_depth[wg_lid] = row_max_depth;
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
if (lid < i)
{
temp_sum += lds[wg_lid + i];
row_max_depth = max(row_max_depth, max_depth[wg_lid + i]);
}
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
}
// at this point, max_depth[thread_0_within_each_wavefront]
// contains the useful maximum depth for this row.
max_depth[wg_lid] = row_max_depth;
// at this point, thread 0's "temp_sum" contains the final useful value.
return temp_sum;
}
// Use a traditional LDS-based reduction to have all of the threads in the wave
// add their values into OUTPUT_THREAD's variable.
// It hides the max work behind the same s_waitcnt on local memory,
// so it should be faster than calling the reduce function three times in a row.
__device__
FPTYPE lds_reduction_three(FPTYPE temp_sum, unsigned int row_max_depth,
unsigned int spin_times, __shared__ FPTYPE *lds,
__shared__ unsigned int *max_depth, __shared__ unsigned int *total_spins,
unsigned int start_of_this_row, unsigned int end_of_this_row,
unsigned int wg_lid)
{
const unsigned int lid = wg_lid % WF_SIZE;
// Have all the threads in a workgroup reduce their data into a single
// value that's then read by the lead thread
// We start by calculating how many layers of reduction we actually need.
// If this is a very short row (smaller than our wavefront size), then we don't need
// to do all iterations of the below loop.
unsigned int num_items = min(end_of_this_row - start_of_this_row - 1, (uint)WF_SIZE);
// find next highest power of two. So if we have 5 things to reduce, we need to
// do a reduction from 8 threads' values. The last 3 will be '0'
num_items = 1 << (CHAR_BIT*(sizeof(unsigned int))-__clz(num_items-1));
for (int i = num_items >> 1; i > 0; i >>= 1)
{
lds[wg_lid] = temp_sum;
max_depth[wg_lid] = row_max_depth;
total_spins[wg_lid] = spin_times;
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
if (lid < i)
{
temp_sum += lds[wg_lid + i];
row_max_depth = max(row_max_depth, max_depth[wg_lid + i]);
spin_times += total_spins[wg_lid + i];
}
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
}
// at this point, max_depth[thread_0_within_each_wavefront]
// contains the useful maximum depth for this row.
max_depth[wg_lid] = row_max_depth;
// and total_spins[thread_0_within_each_wavefront] has its
// total number of spin-loops.
total_spins[wg_lid] = spin_times;
// at this point, thread 0's "temp_sum" contains the final useful value.
return temp_sum;
}
// Do a reduction using bpermute instructions.
// This is strictly worse than Swizzle-based reduction, since it is slower and
// only works on the same hardware as the swizzle instructions.
__device__
FPTYPE bpermute_reduction(FPTYPE temp_sum, unsigned int start_of_this_row,
unsigned int end_of_this_row, unsigned int wg_lid)
{
const unsigned int lid = wg_lid % WF_SIZE;
// Have all the threads in a workgroup reduce their data into a single
// value that's then read by the lead thread
// We start by calculating how many layers of reduction we actually need.
// If this is a very short row (smaller than our workgroup size), then we don't need
// to do all iterations of the below loop.
unsigned int num_items = min(end_of_this_row - start_of_this_row - 1, (uint)WF_SIZE);
// find next highest power of two. So if we have 5 things to reduce, we need to
// do a reduction from 8 threads' values. The last 3 will be '0'
num_items = 1 << (CHAR_BIT*(sizeof(unsigned int))-__clz(num_items-1));
#ifdef USE_DOUBLE
typedef union dbl_b32 {
double val;
uint2 b32;
} dbl_b32_t;
dbl_b32_t t_temp_sum;
t_temp_sum.val = temp_sum;
for (int i = num_items >> 1; i > 0; i >>= 1)
{
int pull_from = (lid + i) << 2;
dbl_b32_t upper_sum;
upper_sum.b32.x = __builtin_amdgcn_ds_bpermute(pull_from, t_temp_sum.b32.x);
upper_sum.b32.y = __builtin_amdgcn_ds_bpermute(pull_from, t_temp_sum.b32.y);
t_temp_sum.val += upper_sum.val;
}
temp_sum = t_temp_sum.val;
#else // !USE_DOUBLE
for (int i = num_items >> 1; i > 0; i >>= 1)
{
uint pull_from = (lid + i) << 2;
temp_sum += as_float(__builtin_amdgcn_ds_bpermute(pull_from, as_uint(temp_sum)));
}
#endif // USE_DOUBLE
return temp_sum;
}
// Do a reduction using bpermute instructions.
// This is strictly worse than Swizzle-based reduction, since it is slower and
// only works on the same hardware as the swizzle instructions.
// This version also does a max-reduce on the row_max_depth variable.
// It hides this bpermute instruction behind the same s_waitcnt on local memory,
// so it should be faster than calling the reduce function twice in a row.
__device__
FPTYPE bpermute_reduction_two(FPTYPE temp_sum, unsigned int *row_max_depth,
unsigned int start_of_this_row, unsigned int end_of_this_row,
unsigned int wg_lid)
{
const unsigned int lid = wg_lid % WF_SIZE;
unsigned int max_depth = *row_max_depth;
// Have all the threads in a workgroup reduce their data into a single
// value that's then read by the lead thread
// We start by calculating how many layers of reduction we actually need.
// If this is a very short row (smaller than our workgroup size), then we don't need
// to do all iterations of the below loop.
unsigned int num_items = min(end_of_this_row - start_of_this_row - 1, (uint)WF_SIZE);
// find next highest power of two. So if we have 5 things to reduce, we need to
// do a reduction from 8 threads' values. The last 3 will be '0'
num_items = 1 << (CHAR_BIT*(sizeof(unsigned int))-__clz(num_items-1));
#ifdef USE_DOUBLE
typedef union dbl_b32 {
double val;
int2 b32;
} dbl_b32_t;
dbl_b32_t t_temp_sum;
t_temp_sum.val = temp_sum;
for (int i = num_items >> 1; i > 0; i >>= 1)
{
int pull_from = (lid + i) << 2;
dbl_b32_t upper_sum;
upper_sum.b32.x = __builtin_amdgcn_ds_bpermute(pull_from, t_temp_sum.b32.x);
upper_sum.b32.y = __builtin_amdgcn_ds_bpermute(pull_from, t_temp_sum.b32.y);
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_bpermute(pull_from, max_depth)));
t_temp_sum.val += upper_sum.val;
}
temp_sum = t_temp_sum.val;
#else // !USE_DOUBLE
for (int i = num_items >> 1; i > 0; i >>= 1)
{
int pull_from = (lid + i) << 2;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_bpermute(pull_from, max_depth)));
temp_sum += as_float(__builtin_amdgcn_ds_bpermute(pull_from, as_uint(temp_sum)));
}
#endif // USE_DOUBLE
*row_max_depth = max_depth;
return temp_sum;
}
// Do a reduction using bpermute instructions.
// This is strictly worse than Swizzle-based reduction, since it is slower and
// only works on the same hardware as the swizzle instructions.
// This version also does a max-reduce on the row_max_depth variable.
// This version also does a max-add on the spin-loops per thread variable.
// It hides this bpermute instruction behind the same s_waitcnt on local memory,
// so it should be faster than calling the reduce function thrice in a row.
__device__
FPTYPE bpermute_reduction_three(FPTYPE temp_sum, unsigned int *row_max_depth,
unsigned int *spin_times, unsigned int start_of_this_row,
unsigned int end_of_this_row, unsigned int wg_lid)
{
const unsigned int lid = wg_lid % WF_SIZE;
unsigned int max_depth = *row_max_depth;
unsigned int spin_time = *spin_times;
// Have all the threads in a workgroup reduce their data into a single
// value that's then read by the lead thread
// We start by calculating how many layers of reduction we actually need.
// If this is a very short row (smaller than our workgroup size), then we don't need
// to do all iterations of the below loop.
unsigned int num_items = min(end_of_this_row - start_of_this_row - 1, (uint)WF_SIZE);
// find next highest power of two. So if we have 5 things to reduce, we need to
// do a reduction from 8 threads' values. The last 3 will be '0'
num_items = 1 << (CHAR_BIT*(sizeof(unsigned int))-__clz(num_items-1));
#ifdef USE_DOUBLE
typedef union dbl_b32 {
double val;
int2 b32;
} dbl_b32_t;
dbl_b32_t t_temp_sum;
t_temp_sum.val = temp_sum;
for (int i = num_items >> 1; i > 0; i >>= 1)
{
int pull_from = (lid + i) << 2;
dbl_b32_t upper_sum;
upper_sum.b32.x = __builtin_amdgcn_ds_bpermute(pull_from, t_temp_sum.b32.x);
upper_sum.b32.y = __builtin_amdgcn_ds_bpermute(pull_from, t_temp_sum.b32.y);
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_bpermute(pull_from, max_depth)));
spin_time += __builtin_amdgcn_ds_bpermute(pull_from, spin_time);
t_temp_sum.val += upper_sum.val;
}
temp_sum = t_temp_sum.val;
#else // !USE_DOUBLE
for (int i = num_items >> 1; i > 0; i >>= 1)
{
int pull_from = (lid + i) << 2;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_bpermute(pull_from, max_depth)));
spin_time += __builtin_amdgcn_ds_bpermute(pull_from, spin_time);
temp_sum += as_float(__builtin_amdgcn_ds_bpermute(pull_from, as_uint(temp_sum)));
}
#endif // USE_DOUBLE
*row_max_depth = max_depth;
*spin_times = spin_time;
return temp_sum;
}
// Swizzle-based reduction; this will work on Sea Islands
/*
FPTYPE swizzle_reduction(FPTYPE temp_sum)
{
#ifdef USE_DOUBLE
typedef union dbl_b32 {
double val;
int2 b32;
} dbl_b32_t;
dbl_b32_t upper_sum, t_temp_sum;
t_temp_sum.val = temp_sum;
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x80b1);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x80b1);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x804e);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x804e);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x101f);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x101f);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x201f);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x201f);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x401f);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x401f);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_readlane(t_temp_sum.b32.x, 32);
upper_sum.b32.y = __builtin_amdgcn_readlane(t_temp_sum.b32.y, 32);
t_temp_sum.val += upper_sum.val;
temp_sum = t_temp_sum.val;
#else // Swizzle-based for SPFP
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x80b1));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x804e));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x101f));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x201f));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x401f));
temp_sum += as_float(__builtin_amdgcn_readlane(as_uint(temp_sum), 32));
#endif // Single or double precision
return temp_sum;
}
// Swizzle-based reduction; this will work on Sea Islands
// This version will also put in a max-reduction for row_max_depth behind
// the s_waitcnt instructions, making it faster than two sequential
// reductions back-to-back.
__device__
FPTYPE swizzle_reduction_two(FPTYPE temp_sum, unsigned int *row_max_depth)
{
#ifdef USE_DOUBLE
typedef union dbl_b32 {
double val;
int2 b32;
} dbl_b32_t;
dbl_b32_t upper_sum, t_temp_sum;
t_temp_sum.val = temp_sum;
unsigned int max_depth = *row_max_depth;
unsigned int upper_max_depth;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x80b1)));
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x80b1);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x80b1);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x804e)));
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x804e);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x804e);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x101f)));
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x101f);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x101f);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x201f)));
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x201f);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x201f);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x401f)));
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x401f);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x401f);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_readlane(max_depth, 32)));
upper_sum.b32.x = __builtin_amdgcn_readlane(t_temp_sum.b32.x, 32);
upper_sum.b32.y = __builtin_amdgcn_readlane(t_temp_sum.b32.y, 32);
t_temp_sum.val += upper_sum.val;
temp_sum = t_temp_sum.val;
#else // Swizzle-based for SPFP
unsigned int max_depth = *row_max_depth;
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x80b1));
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x80b1)));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x804e));
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x804e)));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x101f));
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x101f)));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x201f));
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x201f)));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x401f));
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x401f)));
temp_sum += as_float(__builtin_amdgcn_readlane(as_uint(temp_sum), 32));
max_depth = max(max_depth, as_uint(__builtin_amdgcn_readlane(max_depth, 32)));
#endif // Single or double precision
#ifndef SYNCFREE_KERNEL
*row_max_depth = max_depth;
#endif
return temp_sum;
}
// Swizzle-based reduction; this will work on Sea Islands
// This version will also put in a max-reduction for row_max_depth
// add-reduction of the spin-loop counter behind the s_waitcnt
// instructions, making it faster than two sequential reductions
// back-to-back.
__device__
FPTYPE swizzle_reduction_three(FPTYPE temp_sum, unsigned int *row_max_depth, unsigned int *spin_times)
{
unsigned int max_depth;
unsigned int spins;
#ifdef USE_DOUBLE
typedef union dbl_b32 {
double val;
int2 b32;
} dbl_b32_t;
dbl_b32_t upper_sum, t_temp_sum;
t_temp_sum.val = temp_sum;
max_depth = *row_max_depth;
spins = *spin_times;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x80b1)));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x80b1);
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x80b1);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x80b1);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x804e)));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x804e);
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x804e);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x804e);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x101f)));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x101f);
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x101f);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x101f);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x201f)));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x201f);
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x201f);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x201f);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x401f)));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x401f);
upper_sum.b32.x = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.x, 0x401f);
upper_sum.b32.y = __builtin_amdgcn_ds_swizzle(t_temp_sum.b32.y, 0x401f);
t_temp_sum.val += upper_sum.val;
max_depth = max(max_depth, as_uint(__builtin_amdgcn_readlane(max_depth, 32)));
spins += __builtin_amdgcn_readlane(spins, 32);
upper_sum.b32.x = __builtin_amdgcn_readlane(t_temp_sum.b32.x, 32);
upper_sum.b32.y = __builtin_amdgcn_readlane(t_temp_sum.b32.y, 32);
t_temp_sum.val += upper_sum.val;
temp_sum = t_temp_sum.val;
#else // Swizzle-based for SPFP
max_depth = *row_max_depth;
spins = *spin_times;
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x80b1));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x80b1);
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x80b1)));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x804e));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x804e);
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x804e)));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x101f));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x101f);
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x101f)));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x201f));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x201f);
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x201f)));
temp_sum += as_float(__builtin_amdgcn_ds_swizzle(as_uint(temp_sum), 0x401f));
spins += __builtin_amdgcn_ds_swizzle(spins, 0x401f);
max_depth = max(max_depth, as_uint(__builtin_amdgcn_ds_swizzle(max_depth, 0x401f)));
temp_sum += as_float(__builtin_amdgcn_readlane(as_uint(temp_sum), 32));
spins += __builtin_amdgcn_readlane(spins, 32);
max_depth = max(max_depth, as_uint(__builtin_amdgcn_readlane(max_depth, 32)));
#endif // Single or double precision
*row_max_depth = max_depth;
*spin_times = spins;
return temp_sum;
}
*/
// If we are in GCN3, then use DPP to further increase the performance of
// inter-lane reduction of the temp_sum variable.
__device__
FPTYPE dpp_reduction(FPTYPE temp_sum)
{
// If we write the EXEC mask before the DPP op, we need 5 stall cycles.
// So every one of these starts with an s_nop 4
// We require an s_nop 1 at the end in case the compiler immediately uses
// the last output value.
#ifndef GCN2
#ifdef USE_DOUBLE
typedef struct b32_2 {
int x;
int y;
} b32_t;
typedef union dbl_b32 {
double val;
b32_t b32;
} dbl_b32_t;
dbl_b32_t upper_sum, t_temp_sum;
t_temp_sum.val = temp_sum;
upper_sum.b32.x = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.x, 0x111, 0xf, 0xf, 0); // row_shr:1
upper_sum.b32.y = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.y, 0x111, 0xf, 0xf, 0);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.x, 0x112, 0xf, 0xf, 0); // row_shr:2
upper_sum.b32.y = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.y, 0x112, 0xf, 0xf, 0);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.x, 0x114, 0xf, 0xe, 0); // row_shr:4 bank_mask:0xe
upper_sum.b32.y = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.y, 0x114, 0xf, 0xe, 0);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.x, 0x118, 0xf, 0xc, 0); // row_shr:8 bank_mask:0xc
upper_sum.b32.y = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.y, 0x118, 0xf, 0xc, 0);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.x, 0x142, 0xa, 0xf, 0); // row_bcast:15 row_mask:0xa
upper_sum.b32.y = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.y, 0x142, 0xa, 0xf, 0);
t_temp_sum.val += upper_sum.val;
upper_sum.b32.x = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.x, 0x143, 0xc, 0xf, 0); // row_bcast:31 row_maxk:0xc
upper_sum.b32.y = __builtin_amdgcn_mov_dpp(t_temp_sum.b32.y, 0x143, 0xc, 0xf, 0);
t_temp_sum.val += upper_sum.val;
return t_temp_sum.val;
#else // USE_DOUBLE
__asm__ volatile ("s_nop 4\n"
"v_add_f32 %0 %0 %0 row_shr:1 bound_ctrl:0\n"
"s_nop 1\n"
"v_add_f32 %0 %0 %0 row_shr:2 bound_ctrl:0\n"
"s_nop 1\n"
"v_add_f32 %0 %0 %0 row_shr:4 bank_mask:0xe\n"
"s_nop 1\n"
"v_add_f32 %0 %0 %0 row_shr:8 bank_mask:0xc\n"
"s_nop 1\n"
"v_add_f32 %0 %0 %0 row_bcast:15 row_mask:0xa\n"
"s_nop 1\n"
"v_add_f32 %0 %0 %0 row_bcast:31 row_mask:0xc\n"
"s_nop 1"
: "=v"(temp_sum)
: "0"(temp_sum));
return temp_sum;
#endif // Single vs. Double
#else // We're in GCN2, so we will never enter this function
return temp_sum;
#endif
}
// This version of the DPP reduction function also does a max-reduce on the
// row_max_depth variable. It fits these DPP functions into one of the NOP
// slots required by the DPP instructions, so it should be fast.
__device__
FPTYPE dpp_reduction_two(FPTYPE temp_sum, unsigned int *row_max_depth)
{
// If we write the EXEC mask before the DPP op, we need 5 stall cycles.
// So every one of these starts with an s_nop 4
// We require an s_nop 1 at the end in case the compiler immediately uses
// the last output value.
unsigned int temp_max;
#ifdef USE_DOUBLE
typedef struct b32_2 {
int x;
int y;
} b32_t;
typedef union dbl_b32 {
double val;
b32_t b32;
} dbl_b32_t;
dbl_b32_t upper_sum, t_temp_sum;
temp_max = *row_max_depth;
t_temp_sum.val = temp_sum;
__asm__ volatile ("s_nop 4\n"
"v_mov_b32 %0 %4 row_shr:1 bound_ctrl:0\n"
"v_mov_b32 %1 %5 row_shr:1 bound_ctrl:0\n"
"v_max_u32 %2 %2 %2 row_shr:1 bound_ctrl:0\n"
"s_nop 0\n"
"v_add_f64 %3 %7 %8\n"
"v_mov_b32 %0 %4 row_shr:2 bound_ctrl:0\n"
"v_mov_b32 %1 %5 row_shr:2 bound_ctrl:0\n"
"v_max_u32 %2 %2 %2 row_shr:2 bound_ctrl:0\n"
"s_nop 0\n"
"v_add_f64 %3 %7 %8\n"
"v_mov_b32 %0 %4 row_shr:4 bank_mask:0xe\n"
"v_mov_b32 %1 %5 row_shr:4 bank_mask:0xe\n"
"v_max_u32 %2 %2 %2 row_shr:4 bank_mask:0xe\n"
"s_nop 0\n"
"v_add_f64 %3 %7 %8\n"
"v_mov_b32 %0 %4 row_shr:8 bank_mask:0xc\n"
"v_mov_b32 %1 %5 row_shr:8 bank_mask:0xc\n"
"v_max_u32 %2 %2 %2 row_shr:8 bank_mask:0xc\n"
"s_nop 0\n"
"v_add_f64 %3 %7 %8\n"
"v_mov_b32 %0 %4 row_bcast:15 bank_mask:0xa\n"
"v_mov_b32 %1 %5 row_bcast:15 bank_mask:0xa\n"
"v_max_u32 %2 %2 %2 row_bcast:15 bank_mask:0xa\n"
"s_nop 0\n"
"v_add_f64 %3 %7 %8\n"
"v_mov_b32 %0 %4 row_bcast:31 row_mask:0xc\n"
"v_mov_b32 %1 %5 row_bcast:31 bank_mask:0xc\n"
"v_max_u32 %2 %2 %2 row_bcast:31 bank_mask:0xc\n"
"s_nop 0\n"
"v_add_f64 %3 %7 %8\n"
: "={v2}"(upper_sum.b32.x), "={v3}"(upper_sum.b32.y), "=v"(temp_max), "=v"(t_temp_sum.val)
: "v"(t_temp_sum.b32.x), "v"(t_temp_sum.b32.y), "2"(temp_max), "3"(t_temp_sum.val), "{v[2:3]}"(upper_sum.val));
*row_max_depth = temp_max;
return t_temp_sum.val;
#else
temp_max = *row_max_depth;
__asm__ volatile ("s_nop 4\n"
"v_add_f32 %0 %0 %0 row_shr:1 bound_ctrl:0\n"
"v_max_u32 %1 %1 %1 row_shr:1 bound_ctrl:0\n"
"s_nop 0\n"
"v_add_f32 %0 %0 %0 row_shr:2 bound_ctrl:0\n"
"v_max_u32 %1 %1 %1 row_shr:2 bound_ctrl:0\n"
"s_nop 0\n"
"v_add_f32 %0 %0 %0 row_shr:4 bank_mask:0xe\n"
"v_max_u32 %1 %1 %1 row_shr:4 bank_mask:0xe\n"
"s_nop 0\n"
"v_add_f32 %0 %0 %0 row_shr:8 bank_mask:0xc\n"
"v_max_u32 %1 %1 %1 row_shr:8 bank_mask:0xc\n"
"s_nop 0\n"
"v_add_f32 %0 %0 %0 row_bcast:15 row_mask:0xa\n"
"v_max_u32 %1 %1 %1 row_bcast:15 row_mask:0xa\n"
"s_nop 0\n"
"v_add_f32 %0 %0 %0 row_bcast:31 row_mask:0xc\n"
"v_max_u32 %1 %1 %1 row_bcast:31 row_mask:0xc\n"
"s_nop 1\n"
: "=v"(temp_sum), "=v"(temp_max)
: "0"(temp_sum), "1"(temp_max));
*row_max_depth = temp_max;
return temp_sum;
#endif // Single vs. Double
}
// This version of the DPP reduction function also does a max-reduce on the
// row_max_depth variable and max-add on the total spin variable.
// It fits these DPP functions into NOP slots required by the DPP
// instructions, so it should be fast.
__device__
FPTYPE dpp_reduction_three(FPTYPE temp_sum, unsigned int *row_max_depth, unsigned int *spin_times)
{
// If we write the EXEC mask before the DPP op, we need 5 stall cycles.
// So every one of these starts with an s_nop 4
// We require an s_nop 1 at the end in case the compiler immediately uses
// the last output value.
unsigned int temp_max = *row_max_depth;
unsigned int temp_spin = *spin_times;
#ifdef USE_DOUBLE
typedef struct b32_2 {
int x;
int y;
} b32_t;
typedef union dbl_b32 {
double val;
b32_t b32;
} dbl_b32_t;
dbl_b32_t upper_sum, t_temp_sum;
temp_max = *row_max_depth;
t_temp_sum.val = temp_sum;
__asm__ volatile ("s_nop 4\n"
"v_mov_b32 %0 %5 row_shr:1 bound_ctrl:0\n"
"v_mov_b32 %1 %6 row_shr:1 bound_ctrl:0\n"
"v_max_u32 %2 %2 %2 row_shr:1 bound_ctrl:0\n"
"v_add_u32 %3 " VCC " %3 %3 row_shr:1 bound_ctrl:0\n"
"v_add_f64 %4 %9 %10\n"
"v_mov_b32 %0 %5 row_shr:2 bound_ctrl:0\n"
"v_mov_b32 %1 %6 row_shr:2 bound_ctrl:0\n"
"v_max_u32 %2 %2 %2 row_shr:2 bound_ctrl:0\n"
"v_add_u32 %3 " VCC " %3 %3 row_shr:2 bound_ctrl:0\n"
"v_add_f64 %4 %9 %10\n"
"v_mov_b32 %0 %5 row_shr:4 bank_mask:0xe\n"
"v_mov_b32 %1 %6 row_shr:4 bank_mask:0xe\n"
"v_max_u32 %2 %2 %2 row_shr:4 bank_mask:0xe\n"
"v_add_u32 %3 " VCC " %3 %3 row_shr:4 bank_mask:0xe\n"
"v_add_f64 %4 %9 %10\n"
"v_mov_b32 %0 %5 row_shr:8 bank_mask:0xc\n"
"v_mov_b32 %1 %6 row_shr:8 bank_mask:0xc\n"
"v_max_u32 %2 %2 %2 row_shr:8 bank_mask:0xc\n"
"v_add_u32 %3 " VCC " %3 %3 row_shr:8 bank_mask:0xc\n"
"v_add_f64 %4 %9 %10\n"
"v_mov_b32 %0 %5 row_bcast:15 row_mask:0xa\n"
"v_mov_b32 %1 %6 row_bcast:15 row_mask:0xa\n"
"v_max_u32 %2 %2 %2 row_bcast:15 row_mask:0xa\n"
"v_add_u32 %3 " VCC " %3 %3 row_bcast:15 row_mask:0xa\n"
"v_add_f64 %4 %9 %10\n"
"v_mov_b32 %0 %5 row_bcast:31 row_mask:0xc\n"
"v_mov_b32 %1 %6 row_bcast:31 row_mask:0xc\n"
"v_max_u32 %2 %2 %2 row_bcast:31 row_mask:0xc\n"
"v_add_u32 %3 " VCC " %3 %3 row_bcast:31 row_mask:0xc\n"
"v_add_f64 %4 %9 %10\n"
"s_nop 0\n"
: "={v2}"(upper_sum.b32.x), "={v3}"(upper_sum.b32.y), "=v"(temp_max), "=v"(temp_spin), "=v"(t_temp_sum.val)
: "v"(t_temp_sum.b32.x), "v"(t_temp_sum.b32.y), "2"(temp_max), "3"(temp_spin), "4"(t_temp_sum.val), "{v[2:3]}"(upper_sum.val));
*row_max_depth = temp_max;
*spin_times = temp_spin;
return t_temp_sum.val;
#else
__asm__ volatile ("s_nop 4\n"
"v_add_f32 %0 %0 %0 row_shr:1 bound_ctrl:0\n"
"v_max_u32 %1 %1 %1 row_shr:1 bound_ctrl:0\n"
"v_add_u32 %2 " VCC " %2 %2 row_shr:1 bound_ctrl:0\n"
"v_add_f32 %0 %0 %0 row_shr:2 bound_ctrl:0\n"
"v_max_u32 %1 %1 %1 row_shr:2 bound_ctrl:0\n"
"v_add_u32 %2 " VCC " %2 %2 row_shr:2 bound_ctrl:0\n"
"v_add_f32 %0 %0 %0 row_shr:4 bank_mask:0xe\n"
"v_max_u32 %1 %1 %1 row_shr:4 bank_mask:0xe\n"
"v_add_u32 %2 " VCC " %2 %2 row_shr:4 bank_mask:0xe\n"
"v_add_f32 %0 %0 %0 row_shr:8 bank_mask:0xc\n"
"v_max_u32 %1 %1 %1 row_shr:8 bank_mask:0xc\n"
"v_add_u32 %2 " VCC " %2 %2 row_shr:8 bank_mask:0xc\n"
"v_add_f32 %0 %0 %0 row_bcast:15 row_mask:0xa\n"
"v_max_u32 %1 %1 %1 row_bcast:15 row_mask:0xa\n"
"v_add_u32 %2 " VCC " %2 %2 row_bcast:15\n"
"v_add_f32 %0 %0 %0 row_bcast:31 row_mask:0xc\n"
"v_max_u32 %1 %1 %1 row_bcast:31 row_mask:0xc\n"
"v_add_u32 %2 " VCC " %2 %2 row_bcast:31\n"
"s_nop 1"
: "=v"(temp_sum), "=v"(temp_max), "=v"(temp_spin)
: "0"(temp_sum), "1"(temp_max), "2"(temp_spin));
*row_max_depth = temp_max;
*spin_times = temp_spin;
return temp_sum;
#endif // Single vs. Double
}
// Possible reduction techniques:
//#define LDS_REDUCTION
//#define BPERMUTE_REDUCTION
//#define SWIZZLE_REDUCTION
//#define DPP_REDUCTION
#if defined(GCN2) && defined(DPP_REDUCTION)
#define SWIZZLE_REDUCTION
#undef DPP_REDUCTION
#endif
#ifdef DPP_REDUCTION
#define OUTPUT_THREAD WF_SIZE-1
#else
#define OUTPUT_THREAD 0
#endif
__device__
inline FPTYPE cross_lane_reduction(FPTYPE temp_sum, __shared__ FPTYPE *lds_ptr,
unsigned int start_of_this_row, unsigned int end_of_this_row,
unsigned int wg_lid)
{
#ifdef LDS_REDUCTION
FPTYPE temp_val = lds_reduction(temp_sum, lds_ptr, start_of_this_row,
end_of_this_row, wg_lid);
return temp_val;
#endif
#ifdef BPERMUTE_REDUCTION
return bpermute_reduction(temp_sum, start_of_this_row, end_of_this_row,
wg_lid);
#endif
#ifdef SWIZZLE_REDUCTION
return swizzle_reduction(temp_sum);
#endif
#ifdef DPP_REDUCTION
return dpp_reduction(temp_sum);
#endif
}
__device__
inline FPTYPE cross_lane_reduction_two(FPTYPE temp_sum, unsigned int *row_max_depth,
__shared__ FPTYPE *lds_ptr, __shared__ unsigned int *max_depth_ptr,
unsigned int start_of_this_row, unsigned int end_of_this_row,
unsigned int wg_lid)
{
#ifdef LDS_REDUCTION
FPTYPE temp_val = lds_reduction_two(temp_sum, *row_max_depth, lds_ptr,
max_depth_ptr, start_of_this_row, end_of_this_row, wg_lid);
*row_max_depth = max_depth_ptr[wg_lid & (~(WF_SIZE-1))];
return temp_val;
#endif
#ifdef BPERMUTE_REDUCTION
return bpermute_reduction_two(temp_sum, row_max_depth, start_of_this_row,
end_of_this_row, wg_lid);
#endif
#ifdef SWIZZLE_REDUCTION
return swizzle_reduction_two(temp_sum, row_max_depth);
#endif
#ifdef DPP_REDUCTION
return dpp_reduction_two(temp_sum, row_max_depth);
#endif
}
__device__
inline FPTYPE cross_lane_reduction_three(FPTYPE temp_sum, unsigned int *row_max_depth,
unsigned int *spin_times, __shared__ FPTYPE *lds_ptr,
__shared__ unsigned int *max_depth_ptr, __shared__ unsigned int *total_spins_ptr,
unsigned int start_of_this_row, unsigned int end_of_this_row,
unsigned int wg_lid)
{
#ifdef LDS_REDUCTION
FPTYPE temp_val = lds_reduction_three(temp_sum, *row_max_depth, *spin_times,
lds_ptr, max_depth_ptr, total_spins_ptr, start_of_this_row,
end_of_this_row, wg_lid);
*row_max_depth = max_depth_ptr[wg_lid & (~(WF_SIZE-1))];
*spin_times = total_spins_ptr[wg_lid & (~(WF_SIZE-1))];
return temp_val;
#endif
#ifdef BPERMUTE_REDUCTION
return bpermute_reduction_three(temp_sum, row_max_depth, spin_times,
start_of_this_row, end_of_this_row, wg_lid);
#endif
#ifdef SWIZZLE_REDUCTION
return swizzle_reduction_three(temp_sum, row_max_depth, spin_times);
#endif
#ifdef DPP_REDUCTION
return dpp_reduction_three(temp_sum, row_max_depth, spin_times);
#endif
return temp_sum;
}
// The option below will, in the analyze and syncfree kernels, attempt to
// spin-loop on flags in the LDS for rows that are being solved by wavefronts
// earlier in the same workgroup. This should relieve global memory pressure.
// We found that, with careful control of branching for this logic, this yields
// an average of 20% better performance than global spin-looping.
#define USE_LDS_SPINLOOP
// The option below will, in the levelsync kernel, attempt to spin-loop on
// flags in the LDS for rows that are being solved for wavefronts earlier in
// the same workgroup. This is beneficial if levels have very few rows in them,
// as workgroups are likely to have multiple levels and thus require spinning.
// However, knowing what rows are in the LDS entry is more difficult for the
// levelsync kernel, because it depends entirely on the rowMap entries being
// used by these waves. As such, this loses performance when walking the row
// map outweights the spin-loop benefits. As of this writing, the levelsync
// LDS spin-loop is a net loser.
// Leaving this around for future studies.
// #define USE_LDS_SPINLOOP_LEVELSYNC
// Solves for 'y' in the equation 'A * y = alpha * x'
// In this kernel, we do not know what level each row is in. As such, we must
// dynamically figure this out. Each row has the potential to require data from
// a previous row. This happens when it has a non-zero in a column.
// i.e. having a non-zero value in column $foo means you must wait for row $foo
// to finish.
//
// The 'doneArray' has one entry per row. It starts out with each entry containing
// zeroes. When a row finishes and its output written, it knows its own level
// (which must be 1 more than the highest level of any row it relied on). As such,
// it puts that level into the doneArray. If you must wait on a previous row, you
// spinloop on that row's doneArray entry. Once it's non-zero, you know both that
// the data is ready, as well as what level that value came from (so you can
// calculate your own level).
//
// The doneArray can be used for future iterations, since the parllelism doesn't
// change between iterations. As such, we keep the doneArray around and call
// a different kernel that doesn't do the spin-loop waiting. To prep for that
// kernel, we also need to know how many rows are at each level. Thus, when a
// row finishes, it increments the numRowsAtLevel[] entry associated with its
// level. Also we set the maxDepth variable to the maximum of any level seen.
//__attribute__((reqd_work_group_size(WF_SIZE*WF_PER_WG, 1, 1)))
//__kernel void
__global__ void __launch_bounds__(WF_SIZE * WF_PER_WG, 1)
amd_spts_analyze_and_solve(
const size_t global_work_size,
#ifdef USE_ROC_SHMEM
const int this_pe,
const int total_pes,
unsigned int * __restrict__ shadowDoneArray,
unsigned int * __restrict__ reqUpdateArray,
unsigned int * __restrict__ remoteInProgressArray,
unsigned int * __restrict__ oneBuf,
// 0: Naive puts
// 1: Naive gets
// 2: blocked puts
// 3: put/get hybrid
int roc_shmem_algorithm,
int roc_shmem_put_block_size,
int roc_shmem_get_backoff_factor,
int spts_block_size,
#endif
const FPTYPE * __restrict__ vals,
const int * __restrict__ cols,
const int * __restrict__ rowPtrs,
const FPTYPE * __restrict__ vec_x,
FPTYPE * __restrict__ out_y,
const FPTYPE alpha,
unsigned int * __restrict__ doneArray,
unsigned int * __restrict__ numRowsAtLevel,
unsigned int * __restrict__ maxDepth,
unsigned long long * __restrict__ totalSpin)
{
__shared__ FPTYPE *lds_ptr;
lds_ptr = nullptr;
__shared__ unsigned int *max_depth_ptr;
max_depth_ptr = nullptr;
__shared__ unsigned int *total_spins_ptr;
total_spins_ptr = nullptr;
#ifdef LDS_REDUCTION
__shared__ FPTYPE lds[WF_SIZE*WF_PER_WG];
lds_ptr = lds;
#endif
// If we want future kernel iterations to skip the "wait on previous rows"
// work, we need to know what level set this row is in. This array is used
// to calculate the depth of each dependency so we can calculate max+1.
#ifdef LDS_REDUCTION
__shared__ unsigned int max_depth[WF_SIZE*WF_PER_WG];
max_depth_ptr = max_depth;
__shared__ unsigned int total_spins[WF_SIZE*WF_PER_WG];
total_spins_ptr = total_spins;
#endif // LDS_REDUCTION
unsigned int row_max_depth = 0;
unsigned int spin_times = 0;
const unsigned int wg_lid = hipThreadIdx_x;
const unsigned int lid = wg_lid % WF_SIZE;
#ifdef USE_ROC_SHMEM
__shared__ roc_shmem_ctx_t ctx;
//if (wg_lid == OUTPUT_THREAD) {
roc_shmem_wg_init();
roc_shmem_wg_ctx_create(ROC_SHMEM_CTX_WG_PRIVATE, &ctx);
__syncthreads();
#endif
// Which wavefront within this workgroup
// also means which row within this workgroup's group of rows
const unsigned int local_offset = wg_lid / WF_SIZE;
// First row within this workgroup (within this group of rows)
const unsigned int local_first_row = hipBlockIdx_x * WF_PER_WG;
// Actual row this wavefront will work on.
const unsigned int local_row = local_first_row + local_offset;
#ifdef USE_ROC_SHMEM
// Get the global row for this wavefront assuming a row-cyclic
// decomposition. Basically we need to account for other PEs here.
int local_block_id = local_row / spts_block_size;
const unsigned int block_offset = (local_block_id * spts_block_size * total_pes) +
(this_pe * spts_block_size);
const unsigned int row = block_offset + (local_row % spts_block_size);
const unsigned int first_row = block_offset + (local_first_row % spts_block_size);
#else
const unsigned int row = local_row;
const unsigned int first_row = local_first_row;
#endif
__shared__ FPTYPE diagonal[WF_PER_WG];
#ifdef USE_LDS_SPINLOOP
// If we are trying to access an output that was produced by a wavefront
// earlier in this workgroup, perform the transfer and spin-loop in LDS
// to relieve global memory pressure.
__shared__ unsigned int localDoneArray[WF_PER_WG];
__shared__ FPTYPE localOutY[WF_PER_WG];
__syncthreads();
if (global_work_size > (hipBlockIdx_x * hipBlockDim_x + hipThreadIdx_x)) {
if (lid == 0)
{
localDoneArray[local_offset] = 0;
localOutY[local_offset] = 0.;
}
#else
if (global_work_size > (hipBlockIdx_x * hipBlockDim_x + hipThreadIdx_x)) {
#endif
FPTYPE temp_sum = 0.;
// Preload the first thread with alpha * x. We can bring this forward
// because the 'x' vector in A*y=alpha*x is fixed and known already.
// From this point on, we will subtract out values from rows of X from
// alpha*x, and that will allow us to solve for entries of y.
// Hauling this up to the top of the kernel increases performance because
// it removes the memory load and multiply from the critical path of
// "previous rows' inputs are ready, finish this and allow further rows
// to start up as fast as possible."
if (lid == OUTPUT_THREAD)
temp_sum = alpha * vec_x[row];
unsigned int start_of_this_row = rowPtrs[row];
unsigned int end_of_this_row = rowPtrs[row+1];
unsigned int start_point = start_of_this_row+lid;
// This wavefront operates on a single row, from its beginning to end.
for(unsigned int j = start_point; j < end_of_this_row; j+=WF_SIZE)
{
FPTYPE out_val;
unsigned int local_done = 0;
// Replace the two loads below with inline assembly that sets the
// SLC bit. This forces the loads to essentially bypass the L2
// to increase cache hit rate on other instructions. Vals and cols
// are basically streamed in, so caching them doesn't help much.
// local_col will tell us, for this iteration of the above for loop
// (i.e. for this entry in this row), which columns contain the
// non-zero values. We must then ensure that the output from the row
// associated with the local_col is complete to ensure that we can
// calculate the right answer.
int local_col = __builtin_nontemporal_load(&cols[j]);
// Haul loading from vals[] up near the load of cols[] so that we get
// good coalsced loads.
FPTYPE local_val = __builtin_nontemporal_load(&vals[j]);
// diagonal. Skip this, we need to solve for it.
if (local_col == row)
{
local_done = 1;
diagonal[local_offset] = local_val;
local_val = 0.; // Make the out_val multiply below do nothing.
}
// While there are threads in this workgroup that have been unable to
// get their input, loop and wait for the flag to exist.
__asm__ volatile ("s_setprio 0");
#ifdef USE_ROC_SHMEM
int target_pe = (local_col / spts_block_size) % total_pes;
int backoff_counter = 0;
bool need_remote_notify = true;
bool need_comm = true;
bool first_time = true;
#endif
#ifdef USE_LDS_SPINLOOP
if (local_col >= first_row)
{
while (!local_done)
{
// Check in the LDS if the value was produced by someone
// within this workgroup.
local_done = localDoneArray[local_col - first_row];
out_val = localOutY[local_col - first_row];
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
}
}
#endif // USE_LDS_SPINLOOP
while (!local_done)
{
// Replace this atomic with an assembly load with GLC bit set.
// This forces the load to go to the coherence point, allowing
// us to avoid deadlocks.
// local_done = atomic_get_done(doneArray, local_col);
__asm__ volatile (MEM_PREFIX"_load_dword %0 %1 " OFF_MODIFIER " glc slc\n"
"s_waitcnt vmcnt(0)"
: "=v"(local_done)
: "v"(&doneArray[local_col]));
spin_times++;
#ifdef USE_ROC_SHMEM
if ((total_pes > 1) && (target_pe != this_pe) && (roc_shmem_algorithm == 1)) {
if (first_time) {
if (atomicCAS(&remoteInProgressArray[local_col], 0, 1) != 0)
need_comm = false;
}
first_time = false;
if (need_comm)
{
for (int i = 0; i < (backoff_counter * roc_shmem_get_backoff_factor); i++)
__asm__ volatile("s_sleep 127");
roc_shmem_ctx_getmem_nbi(ctx, &shadowDoneArray[local_col], &doneArray[local_col], sizeof(int), target_pe);
//roc_shmem_ctx_quiet(ctx);
__asm__ volatile (MEM_PREFIX"_load_dword %0 %1 " OFF_MODIFIER " glc slc\n"
"s_waitcnt vmcnt(0)"
: "=v"(local_done)
: "v"(&shadowDoneArray[local_col]));
if (local_done)
{
roc_shmem_ctx_getmem_nbi(ctx, &out_y[local_col], &out_y[local_col], sizeof(FPTYPE), target_pe);
__asm__ volatile (MEM_PREFIX"_store_dword %0 %1 " OFF_MODIFIER " glc\n" WAKEUP
:
: "v"(&doneArray[local_col]),
"v"(local_done));
} else {
backoff_counter++;
}
}
}
if ((total_pes > 1) && (target_pe != this_pe) && (roc_shmem_algorithm == 3)) {
if (need_remote_notify) {
need_remote_notify = false;
//if (atomicCAS(&remoteInProgressArray[local_col], 0, 1) != 0)
//if (atomicCAS(&remoteInProgressArray[local_col], 0, 1) == 0)
{
roc_shmem_ctx_putmem_nbi(ctx, &reqUpdateArray[local_col], oneBuf, sizeof(int), target_pe);
//printf("Put 111 blockIDx %d threadID %d target_pe %d local_col %d oneBuf[0]= %d \n", hipBlockIdx_x, hipThreadIdx_x, target_pe, local_col, oneBuf[0]);
roc_shmem_ctx_fence(ctx);
//printf("fence 222 blockIDx %d threadID %d target_pe %d local_col %d \n", hipBlockIdx_x, hipThreadIdx_x, target_pe, local_col);
roc_shmem_ctx_getmem_nbi(ctx, &shadowDoneArray[local_col], &doneArray[local_col], sizeof(int), target_pe);
roc_shmem_ctx_quiet(ctx);
//printf("Get 333 blockIDx %d threadID %d target_pe %d local_col %d shadowDone %d \n \n", hipBlockIdx_x, hipThreadIdx_x, target_pe, local_col, shadowDoneArray[local_col]);
__asm__ volatile (MEM_PREFIX"_load_dword %0 %1 " OFF_MODIFIER " glc slc\n"
"s_waitcnt vmcnt(0)"
: "=v"(local_done)
: "v"(&shadowDoneArray[local_col]));
if (local_done)
{
roc_shmem_ctx_getmem_nbi(ctx, &out_y[local_col], &out_y[local_col], sizeof(FPTYPE), target_pe);
roc_shmem_ctx_quiet(ctx);
__asm__ volatile (MEM_PREFIX"_store_dword %0 %1 " OFF_MODIFIER " glc\n" WAKEUP
:
: "v"(&doneArray[local_col]),
"v"(local_done));
}
}
}
}
#endif
}
__asm__ volatile ("s_setprio 1");
#ifdef USE_LDS_SPINLOOP
if (local_col < first_row)
#endif
{
// The command below is manually replaced with GCN assembly with
// the GLC bit set. This bypasses the L1, allowing us to do a
// coherent load of the variable without needing atomics.
#ifdef USE_DOUBLE
// out_val = as_double(atom_or((__global ulong *)&(out_y[local_col]), 0));
__asm__ volatile (MEM_PREFIX"_load_dwordx2 %0 %1 " OFF_MODIFIER " glc\n"
"s_waitcnt vmcnt(0)"
: "=v"(out_val)
: "v"(&out_y[local_col]));
#else
// out_val = as_float(atomic_or((__global uint *)&(out_y[local_col]), 0));
__asm__ volatile (MEM_PREFIX"_load_dword %0 %1 " OFF_MODIFIER " glc\n"
"s_waitcnt vmcnt(0)"
: "=v"(out_val)
: "v"(&out_y[local_col]));
#endif
}
temp_sum -= local_val * out_val;
row_max_depth = max(local_done, row_max_depth);
}
__asm__ volatile ("s_setprio 1");
// And if we care about the maximum depth, add it into OUTPUT_THREAD's
// entry within the max_depth array.
temp_sum = cross_lane_reduction_three(temp_sum, &row_max_depth, &spin_times,
lds_ptr, max_depth_ptr, total_spins_ptr, start_of_this_row,
end_of_this_row, wg_lid);
row_max_depth++;
// y = (x-sum_of_vals_from_A) / diag
if (lid == OUTPUT_THREAD)
{
#ifndef LDS_REDUCTION
// Wait for local memory to quiesce for the diagonal
// LDS_REDUCTION has such waits in it already.
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
#endif
FPTYPE out_val = temp_sum / diagonal[local_offset];
//out_y[row] = out_val;
#ifdef USE_DOUBLE
__asm__ volatile (MEM_PREFIX"_store_dwordx2 %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : : "v" (&out_y[row]), "v"(out_val));
#else
__asm__ volatile (MEM_PREFIX"_store_dword %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : : "v" (&out_y[row]), "v"(out_val));
#endif
//out_y[row] = temp_sum / diagonal[local_offset]; // original divide
#ifdef USE_LDS_SPINLOOP
localOutY[row - first_row] = out_val;
localDoneArray[row - first_row] = row_max_depth;
#endif // USE_LDS_SPINLOOP
//doneArray[row] = row_max_depth;
__asm__ volatile (MEM_PREFIX"_store_dword %0 %1 " OFF_MODIFIER " glc\n" WAKEUP : : "v"(&doneArray[row]), "v"(row_max_depth));
asm volatile ("s_waitcnt vmcnt(0)\n\t");
#ifdef USE_ROC_SHMEM
if (roc_shmem_algorithm == 2 && total_pes > 1) {
int CHUNK = roc_shmem_put_block_size;
bool sendTime = true;
int row_base = (row / CHUNK) * CHUNK;
int num_done = atomicAdd(&shadowDoneArray[row_base], 1);
sendTime = (num_done == (CHUNK - 1));
for(int p=0; p<total_pes; p++){
if(p != this_pe && sendTime){
roc_shmem_ctx_putmem_nbi(ctx, &out_y[row_base], &out_y[row_base], sizeof(FPTYPE) * CHUNK, p);
roc_shmem_ctx_fence(ctx);
roc_shmem_ctx_putmem_nbi(ctx, &doneArray[row_base], &doneArray[row_base], sizeof(int) * CHUNK, p);
roc_shmem_ctx_quiet(ctx);
}
}
}
if (roc_shmem_algorithm == 0) {
for(int p=0; p<total_pes; p++){
if(p != this_pe){
roc_shmem_ctx_putmem_nbi(ctx, &out_y[row], &out_y[row], sizeof(FPTYPE), p);
roc_shmem_ctx_fence(ctx);
roc_shmem_ctx_putmem_nbi(ctx, &doneArray[row], &doneArray[row], sizeof(int), p);
}
}
}
if (roc_shmem_algorithm == 3) {
// Only broadcast update if another node explicitly registered for this row. TODO:
// Make 2D array to scale
unsigned int need_broadcast;
__asm__ volatile (MEM_PREFIX"_load_dword %0 %1 " OFF_MODIFIER " glc slc\ns_waitcnt vmcnt(0)" : "=v"(need_broadcast) : "v"(&reqUpdateArray[row]));
if (need_broadcast == 1) {
for(int p=0; p<total_pes; p++) {
if (p != this_pe) {
roc_shmem_ctx_putmem_nbi(ctx, &out_y[row], &out_y[row], sizeof(FPTYPE), p);
roc_shmem_ctx_fence(ctx);
roc_shmem_ctx_putmem_nbi(ctx, &doneArray[row], &doneArray[row], sizeof(int), p);
}
}
}
}
#endif
// Must atomic these next two, since other WGs are doing the same thing
// We're sending out "row_max_depth-1" because of 0-based indexing.
// However, we needed to put a non-zero value into the doneArray up above
// when we crammed row_max_depth in, so these two will be off by one.
atomicAdd(&numRowsAtLevel[row_max_depth-1], 1);
atomicMax(maxDepth, row_max_depth);
atomicAdd(totalSpin, spin_times);
// If you add this back in after doing a native_divide up above,
// we can get *some* of the accuracy of a full Newton-Raphson
// divide while maintaining the performance of the
// native_divide() on the critical path.
//out_y[row] = temp_sum / diagonal[local_offset];
}
}
#ifdef USE_ROC_SHMEM
__syncthreads();
//if (wg_lid == OUTPUT_THREAD)
roc_shmem_wg_ctx_destroy(ctx);
roc_shmem_wg_finalize();
#endif
}
// Solves for 'y' in the equation 'A * y = alpha * x'
// In this kernel, we do not know what level each row is in. As such, we must
// dynamically figure this out. Each row has the potential to require data from
// a previous row. This happens when it has a non-zero in a column.
// i.e. having a non-zero value in column $foo means you must wait for row $foo
// to finish.
//
// The 'doneArray' has one entry per row. It starts out with each entry containing
// zeroes. When a row finishes and its output written, it knows its own level
// (which must be 1 more than the highest level of any row it relied on). As such,
// it puts that level into the doneArray. If you must wait on a previous row, you
// spinloop on that row's doneArray entry. Once it's non-zero, you know both that
// the data is ready, as well as what level that value came from (so you can
// calculate your own level).
//
// The doneArray can be used for future iterations, since the parllelism doesn't
// change between iterations. As such, we keep the doneArray around and call
// a different kernel that doesn't do the spin-loop waiting. To prep for that
// kernel, we also need to know how many rows are at each level. Thus, when a
// row finishes, it increments the numRowsAtLevel[] entry associated with its
// level. Also we set the maxDepth variable to the maximum of any level seen.
//__attribute__((reqd_work_group_size(WF_SIZE*WF_PER_WG, 1, 1)))
//__kernel void
__global__ void __launch_bounds__(WF_SIZE * WF_PER_WG, 1)
amd_spts_syncfree_solve(
size_t global_work_size,
const FPTYPE * __restrict__ vals,
const int * __restrict__ cols,
const int * __restrict__ rowPtrs,
const FPTYPE * __restrict__ vec_x,
FPTYPE * __restrict__ out_y,
const FPTYPE alpha,
unsigned int * __restrict__ doneArray,
unsigned int * __restrict__ numRowsAtLevel,
unsigned int * __restrict__ maxDepth,
unsigned long long * __restrict__ totalSpin)
{
if (global_work_size <= hipBlockIdx_x * hipBlockDim_x + hipThreadIdx_x) return;
__shared__ FPTYPE *lds_ptr;
lds_ptr = nullptr;
__shared__ unsigned int *max_depth_ptr;
max_depth_ptr = nullptr;
__shared__ unsigned int *total_spins_ptr;
total_spins_ptr = nullptr;
#ifdef LDS_REDUCTION
__shared__ FPTYPE lds[WF_SIZE*WF_PER_WG];
lds_ptr = lds;
#endif
const unsigned int wg_lid = hipThreadIdx_x;
const unsigned int lid = wg_lid % WF_SIZE;
// Which wavefront within this workgroup
// also means which row within this workgroup's group of rows
const unsigned int local_offset = wg_lid / WF_SIZE;
// First row within this workgroup (within this group of rows)
const unsigned int first_row = hipBlockIdx_x * WF_PER_WG;
// Actual row this wavefront will work on.
const unsigned int row = first_row + local_offset;
__shared__ FPTYPE diagonal[WF_PER_WG];
#ifdef USE_LDS_SPINLOOP
// If we are trying to access an output that was produced by a wavefront
// earlier in this workgroup, perform the transfer and spin-loop in LDS
// to relieve global memory pressure.
__shared__ unsigned int localDoneArray[WF_PER_WG];
__shared__ FPTYPE localOutY[WF_PER_WG];
#endif
FPTYPE temp_sum = 0.;
// Preload the first thread with alpha * x. We can bring this forward
// because the 'x' vector in A*y=alpha*x is fixed and known already.
// From this point on, we will subtract out values from rows of X from
// alpha*x, and that will allow us to solve for entries of y.
// Hauling this up to the top of the kernel increases performance because
// it removes the memory load and multiply from the critical path of
// "previous rows' inputs are ready, finish this and allow further rows
// to start up as fast as possible."
if (lid == OUTPUT_THREAD)
temp_sum = alpha * vec_x[row];
unsigned int start_of_this_row = rowPtrs[row];
unsigned int end_of_this_row = rowPtrs[row+1];
unsigned int start_point = start_of_this_row+lid;
// This wavefront operates on a single row, from its beginning to end.
for(unsigned int j = start_point; j < end_of_this_row; j+=WF_SIZE)
{
#ifdef USE_LDS_SPINLOOP
if (lid == 0)
{
localDoneArray[local_offset] = 0;
localOutY[local_offset] = 0.;
}
#endif
// local_col will tell us, for this iteration of the above for loop
// (i.e. for this entry in this row), which columns contain the
// non-zero values. We must then ensure that the output from the row
// associated with the local_col is complete to ensure that we can
// calculate the right answer.
int local_col = -1;
// Haul loading from vals[] up near the load of cols[] so that we get
// good coalsced loads.
FPTYPE local_val = 0.;
unsigned int local_done = 0;
// Replace the two loads below with inline assembly that sets the
// SLC bit. This forces the loads to essentially bypass the L2
// to increase cache hit rate on other instructions. Vals and cols
// are basically streamed in, so caching them doesn't help much.
// local_col = cols[j];
// local_val = vals[j];
#ifdef USE_DOUBLE
__asm__ volatile (MEM_PREFIX"_load_dword %0 %2 " OFF_MODIFIER " slc\n" MEM_PREFIX"_load_dwordx2 %1 %3 " OFF_MODIFIER " slc\ns_waitcnt vmcnt(0)" : "=v"(local_col), "=v"(local_val) : "v"(&cols[j]), "v"(&vals[j]));
#else
__asm__ volatile (MEM_PREFIX"_load_dword %0 %2 " OFF_MODIFIER " slc\n" MEM_PREFIX"_load_dword %1 %3 " OFF_MODIFIER " slc\ns_waitcnt vmcnt(0)" : "=v"(local_col), "=v"(local_val) : "v"(&cols[j]), "v"(&vals[j]));
#endif
// diagonal. Skip this, we need to solve for it.
if (local_col == row)
{
local_done = 1;
diagonal[local_offset] = local_val;
}
// While there are threads in this workgroup that have been unable to
// get their input, loop and wait for the flag to exist.
__asm__ volatile ("s_setprio 0");
while (!local_done)
{
#ifdef USE_LDS_SPINLOOP
if (local_col >= first_row)
{
// Check in the LDS if the value was produced by someone
// within this workgroup.
local_done = localDoneArray[local_col - first_row];
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
}
else
#endif // USE_LDS_SPINLOOP
{
// Replace this atomic with an assembly load with GLC bit set.
// This forces the load to go to the coherence point, allowing
// us to avoid deadlocks.
// local_done = atomic_get_done(doneArray, local_col);
__asm__ volatile (MEM_PREFIX"_load_dword %0 %1 " OFF_MODIFIER " glc slc\ns_waitcnt vmcnt(0)" : "=v"(local_done) : "v"(&doneArray[local_col]));
}
if (local_done)
{
FPTYPE out_val;
__asm__ volatile ("s_setprio 1");
#ifdef USE_LDS_SPINLOOP
if (local_col >= first_row)
{
out_val = localOutY[local_col - first_row];
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
}
else
#endif // USE_LDS_SPINLOOP
{
// The command below is manually replaced with GCN assembly with
// the GLC bit set. This bypasses the L1, allowing us to do a
// coherent load of the variable without needing atomics.
#ifdef USE_DOUBLE
// out_val = as_double(atom_or((__global ulong *)&(out_y[local_col]), 0));
__asm__ volatile (MEM_PREFIX"_load_dwordx2 %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : "=v"(out_val) : "v"(&out_y[local_col]));
#else
// out_val = as_float(atomic_or((__global uint *)&(out_y[local_col]), 0));
__asm__ volatile (MEM_PREFIX"_load_dword %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : "=v"(out_val) : "v"(&out_y[local_col]));
#endif
}
temp_sum -= local_val * out_val;
}
else
{
(void)0;
}
}
}
__asm__ volatile ("s_setprio 1");
// Take all of the temp_sum values and add them together into
// OUTPUT_THREAD's temp_sum value.
temp_sum = cross_lane_reduction(temp_sum, lds_ptr, start_of_this_row,
end_of_this_row, wg_lid);
// y = (x-sum_of_vals_from_A) / diag
if (lid == OUTPUT_THREAD)
{
#ifndef LDS_REDUCTION
// Wait for local memory to quiesce for the diagonal
// LDS_REDUCTION has such waits in it already.
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
#endif
FPTYPE out_val = temp_sum / diagonal[local_offset];
//out_y[row] = out_val;
#ifdef USE_DOUBLE
__asm__ volatile (MEM_PREFIX"_store_dwordx2 %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : : "v" (&out_y[row]), "v"(out_val));
#else
__asm__ volatile (MEM_PREFIX"_store_dword %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : : "v" (&out_y[row]), "v"(out_val));
#endif
//out_y[row] = temp_sum / diagonal[local_offset]; // original divide
int set_one = 1;
#ifdef USE_LDS_SPINLOOP
localDoneArray[row - first_row] = 1;
localOutY[row - first_row] = out_val;
#endif // USE_LDS_SPINLOOP
//doneArray[row] = 1;
__asm__ volatile (MEM_PREFIX"_store_byte %0 %1 " OFF_MODIFIER " glc\n" WAKEUP : : "v"(&doneArray[row]), "v"(set_one));
// If you add this back in after doing a native_divide up above,
// we can get *some* of the accuracy of a full Newton-Raphson
// divide while maintaining the performance of the
// native_divide() on the critical path.
//out_y[row] = temp_sum / diagonal[local_offset];
}
}
// Solves for 'y' in the equation 'A * y = alpha * x'
// In this kernel, every row is in the same level. As such, we can freely
// have every workgrup complete at its own pace.
// However, we must call this kernel multiple times, once per level.
//
// The rowMap tells us that, in this level, gid X works on row Y.
// We need this because each level of the solve can have different numbers
// of non-contiguous row. This version of our solver uses one kernel call
// per level.
//
// In addition, the 'total_rows_in_prev_levels' tells us how far in that array
// to look.
__global__ void __launch_bounds__(WF_SIZE * WF_PER_WG, 1)
amd_spts_levelset_solve(
size_t global_work_size,
const FPTYPE * __restrict__ vals,
const int * __restrict__ cols,
const int * __restrict__ rowPtrs,
const FPTYPE * __restrict__ vec_x,
FPTYPE * __restrict__ out_y,
const unsigned int * __restrict__ rowMap,
const unsigned int total_rows_in_prev_levels,
const FPTYPE alpha)
{
if (global_work_size <= hipBlockIdx_x * hipBlockDim_x + hipThreadIdx_x) return;
__shared__ FPTYPE *lds_ptr;
lds_ptr = nullptr;
#ifdef LDS_REDUCTION
__shared__ FPTYPE lds[WF_SIZE*WF_PER_WG];
lds_ptr = lds;
#endif
// Which wavefront within this workgroup
// also means which row within this workgroup's group of rows
const unsigned int local_offset = hipThreadIdx_x / WF_SIZE;
// First row within this workgroup (within this group of rows)
const unsigned int first_row = hipBlockIdx_x * WF_PER_WG;
const unsigned int wg_lid = hipThreadIdx_x;
const unsigned int lid = wg_lid % WF_SIZE;
const unsigned int row = rowMap[total_rows_in_prev_levels+first_row+local_offset];
__shared__ FPTYPE diagonal[WF_PER_WG];
FPTYPE temp_sum = 0.;
// Preload the first thread with alpha * x. We can bring this forward
// because the 'x' vector in A*y=alpha*x is fixed and known already.
// From this point on, we will subtract out values from rows of X from
// alpha*x, and that will allow us to solve for entries of y.
if (lid == OUTPUT_THREAD)
temp_sum = alpha * vec_x[row];
unsigned int start_of_this_row = rowPtrs[row];
unsigned int end_of_this_row = rowPtrs[row+1];
unsigned int start_point = start_of_this_row+lid;
// This workgroup operates on a single row, from its beginning to end.
for(unsigned int j = start_point; j < end_of_this_row; j+=WF_SIZE)
{
// local_col will tell us, for this iteration of the above for loop
// (i.e. for this entry in this row), which columns contain the
// non-zero values. We must then ensure that the output from the row
// associated with the local_col is complete to ensure that we can
// calculate the right answer.
int local_col = -1;
// Haul loading from vals[] up near the load of cols[] so that we get
// good coalsced loads.
FPTYPE local_val = 0.;
// Replace the two loads below with inline assembly that sets the
// SLC bit. This forces the loads to essentially bypass the L2
// to increase cache hit rate on other instructions. Vals and cols
// are basically streamed in, so caching them doesn't help much.
// local_col = cols[j];
// local_val = vals[j];
#ifdef USE_DOUBLE
__asm__ volatile (MEM_PREFIX"_load_dword %0 %2 " OFF_MODIFIER " slc\n" MEM_PREFIX"_load_dwordx2 %1 %3 " OFF_MODIFIER " slc\ns_waitcnt vmcnt(0)" : "=v"(local_col), "=v"(local_val) : "v"(&cols[j]), "v"(&vals[j]));
#else
__asm__ volatile (MEM_PREFIX"_load_dword %0 %2 " OFF_MODIFIER " slc\n" MEM_PREFIX"_load_dword %1 %3 " OFF_MODIFIER " slc\ns_waitcnt vmcnt(0)" : "=v"(local_col), "=v"(local_val) : "v"(&cols[j]), "v"(&vals[j]));
#endif
// diagonal. Skip this, we need to solve for it.
if (local_col == row)
diagonal[local_offset] = local_val;
else
{
FPTYPE out_val = out_y[local_col];
temp_sum -= local_val * out_val;
}
}
// Take all of the temp_sum values and add them together into
// OUTPUT_THREAD's temp_sum value.
temp_sum = cross_lane_reduction(temp_sum, lds_ptr,
start_of_this_row, end_of_this_row, wg_lid);
// y = (x-sum_of_vals_from_A) / diag
if (lid == OUTPUT_THREAD)
{
#ifndef LDS_REDUCTION
// Wait for local memory to quiesce for the diagonal
// LDS_REDUCTION has such waits in it already.
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
#endif
out_y[row] = temp_sum / diagonal[local_offset]; // original divide
//out_y[row] = temp_sum / diagonal[local_offset]; // original divide
}
}
// Solves for 'y' in the equation 'A * y = alpha * x'
// This kernel will only work if we launch a single workgroup that will
// solve multiple levels in a serial fashion. For each level, every thread
// within that level will try to solve for a different row.
// After solving for this level, the single workgroup hits a workgroup-wide
// barrier instruction waiting for all the other rows in this level to
// complete.
//
// We can only solve up to 1024 rows in a single level call right now,
// because each thread will solve a single row per level.
//
// This is a "CSR-Scalar" style analysis, where each thread is accessing
// a potentially very different area of both the CSR matrix and the vector.
// Performance may be bad, but this is very easy to write.
//
// The rowMap tells us that, within a level, thread X works on row Y.
// We need this because each level of the solve can have different numbers
// of non-contiguous row.
// In addition, the 'total_rows_in_prev_levels' tells us how far in that array
// to look.
//
// [start_level, end_level) tell us which entries in the rowMap we will go
// through in this kernel invocation.
__global__ void __launch_bounds__(WF_SIZE * WF_PER_WG, 1)
amd_spts_scalar_solve(
size_t global_work_size,
const FPTYPE * __restrict__ vals,
const int * __restrict__ cols,
const int * __restrict__ rowPtrs,
const FPTYPE * __restrict__ vec_x,
FPTYPE * __restrict__ out_y,
const FPTYPE alpha,
const unsigned int * __restrict__ rowMap,
const unsigned int * __restrict__ totalRowsInEachLevel,
const unsigned int total_rows_in_prev_levels,
const unsigned int start_level,
const unsigned int end_level)
{
if (global_work_size <= hipBlockIdx_x * hipBlockDim_x + hipThreadIdx_x) return;
const unsigned int gid = hipBlockIdx_x;
const unsigned int wg_lid = hipThreadIdx_x;
const unsigned int lid = wg_lid % WF_SIZE;
__shared__ unsigned int total_rows_seen_so_far;
if (wg_lid == 0)
total_rows_seen_so_far = 0;
// We have a single workgroup, and it is going to walk through a
// contiguous set of "levels" in the dependency graph.
for (unsigned int current_level = start_level; current_level < end_level; current_level++)
{
// Every time we reach a new level, all of the threads within
// this workgroup need to have completed their row's work.
// This guarantees that we have synchronized.
__syncthreads();
if (wg_lid < totalRowsInEachLevel[current_level])
{
const unsigned int entry_in_row_map = total_rows_in_prev_levels + total_rows_seen_so_far + wg_lid;
const unsigned int row = rowMap[entry_in_row_map];
FPTYPE diagonal = 0.;
FPTYPE temp_sum = alpha * vec_x[row];
unsigned int start_of_this_row = rowPtrs[row];
unsigned int end_of_this_row = rowPtrs[row+1];
// This thread operates on a single row, from its beginning to end.
for(unsigned int j = start_of_this_row; j < end_of_this_row; j++)
{
// local_col will tell us, for this iteration of the above for loop
// (i.e. for this entry in this row), which columns contain the
// non-zero values. We must then ensure that the output from the row
// associated with the local_col is complete to ensure that we can
// calculate the right answer.
int local_col = cols[j];
// Haul loading from vals[] up near the load of cols[] so that we get
// good coalsced loads.
FPTYPE local_val = vals[j];
// diagonal. Skip this, we need to solve for it.
if (local_col == row)
diagonal = local_val;
else
{
FPTYPE out_val;
#ifdef USE_DOUBLE
out_val = __ull2double_rd(atomicOr((unsigned long long *)&(out_y[local_col]), 0));
#else
out_val = as_float(atomicOr((uint *)&(out_y[local_col]), 0));
#endif
temp_sum -= local_val * out_val;
}
}
FPTYPE out_val = temp_sum / diagonal;
//FPTYPE out_val = temp_sum / diagonal; // original divide
out_y[row] = out_val;
}
if (wg_lid == 0)
total_rows_seen_so_far += totalRowsInEachLevel[current_level];
}
}
// Solves for 'y' in the equation 'A * y = alpha * x'
// This kernel will only work if we launch a single workgroup that will
// solve multiple levels in a serial fashion. For each level, every wavefront
// within that level will try to solve for a different row.
// After solving for this level, the single workgroup hits a workgroup-wide
// barrier instruction waiting for all the other rows in this level to
// complete.
//
// Within a level, this algorithm will loop through the rows, so we should
// be able to handle levels of any size -- no synchronization is needed
// between the wavefronts working on a single level, since those rows are
// independent of one another.
//
// This is a "CSR-Vector" style execution, where each wavefront accesses
// coalesced values within its row, but where short rows waste thread
// resources.
//
// The rowMap tells us that, within a level, thread X works on row Y.
// We need this because each level of the solve can have different numbers
// of non-contiguous row.
// In addition, the 'total_rows_in_prev_levels' tells us how far in that array
// to look.
//
// [start_level, end_level) tell us which entries in the rowMap we will go
// through in this kernel invocation.
__global__ void __launch_bounds__(WF_SIZE * WF_PER_WG, 1)
amd_spts_vector_solve(
size_t global_work_size,
const FPTYPE * __restrict__ vals,
const int * __restrict__ cols,
const int * __restrict__ rowPtrs,
const FPTYPE * __restrict__ vec_x,
FPTYPE * out_y,
const FPTYPE alpha,
const unsigned int * __restrict__ rowMap,
const unsigned int * __restrict__ totalRowsInEachLevel,
const unsigned int total_rows_in_prev_levels,
const unsigned int start_level,
const unsigned int end_level)
{
if (global_work_size <= hipBlockIdx_x * hipBlockDim_x + hipThreadIdx_x) return;
__shared__ FPTYPE *lds_ptr;
lds_ptr = nullptr;
#ifdef LDS_REDUCTION
__shared__ FPTYPE lds[WF_SIZE*WF_PER_WG];
lds_ptr = lds;
#endif
__shared__ FPTYPE diagonal[WF_PER_WG];
// First row within this workgroup (within this group of rows)
const unsigned int first_row = hipBlockIdx_x * WF_PER_WG;
const unsigned int gid = hipBlockIdx_x;
const unsigned int wg_lid = hipThreadIdx_x;
const unsigned int lid = wg_lid % WF_SIZE;
const unsigned int wf_id = wg_lid / WF_SIZE;
unsigned int cur_loc_row = wf_id;
unsigned int total_rows_seen_so_far = 0;
// We have a single workgroup, and it is going to walk through a
// contiguous set of "levels" in the dependency graph.
for (unsigned int current_level = start_level; current_level < end_level; current_level++)
{
// Every time we reach a new level, all of the wavefronts within
// this workgroup need to have completed their row's work.
// This guarantees that we have synchronized.
__syncthreads();
for (unsigned int cur_loc_row = wf_id; cur_loc_row < totalRowsInEachLevel[current_level]; cur_loc_row += WF_PER_WG)
{
const unsigned int entry_in_row_map = total_rows_in_prev_levels + total_rows_seen_so_far + cur_loc_row;
const unsigned int row = rowMap[entry_in_row_map];
FPTYPE temp_sum = 0.;
if (lid == OUTPUT_THREAD)
temp_sum = alpha * vec_x[row];
unsigned int start_of_this_row = rowPtrs[row];
unsigned int end_of_this_row = rowPtrs[row+1];
// This thread operates on a single row, from its beginning to end.
for(unsigned int j = start_of_this_row + lid; j < end_of_this_row; j += WF_SIZE)
{
// local_col will tell us, for this iteration of the above for loop
// (i.e. for this entry in this row), which columns contain the
// non-zero values. We must then ensure that the output from the row
// associated with the local_col is complete to ensure that we can
// calculate the right answer.
int local_col = -1;
// Haul loading from vals[] up near the load of cols[] so that we get
// good coalsced loads.
FPTYPE local_val = 0.;
// Replace the two loads below with inline assembly that sets the
// SLC bit. This forces the loads to essentially bypass the L2
// to increase cache hit rate on other instructions. Vals and cols
// are basically streamed in, so caching them doesn't help much.
//local_col = cols[j];
//local_val = vals[j];
#ifdef USE_DOUBLE
__asm__ volatile (MEM_PREFIX"_load_dword %0 %2 " OFF_MODIFIER " slc\n" MEM_PREFIX"_load_dwordx2 %1 %3 " OFF_MODIFIER " slc\ns_waitcnt vmcnt(0)" : "=v"(local_col), "=v"(local_val) : "v"(&cols[j]), "v"(&vals[j]));
#else
__asm__ volatile (MEM_PREFIX"_load_dword %0 %2 " OFF_MODIFIER " slc\n" MEM_PREFIX"_load_dword %1 %3 " OFF_MODIFIER " slc\ns_waitcnt vmcnt(0)" : "=v"(local_col), "=v"(local_val) : "v"(&cols[j]), "v"(&vals[j]));
#endif
// diagonal. Skip this, we need to solve for it.
if (local_col == row)
diagonal[wf_id] = local_val;
else
{
FPTYPE out_val;
out_val = out_y[local_col];
temp_sum -= local_val * out_val;
}
}
// Take all of the temp_sum values and add them together into
// OUTPUT_THREAD's temp_sum value.
temp_sum = cross_lane_reduction(temp_sum, lds_ptr,
start_of_this_row, end_of_this_row, wg_lid);
// y = (x-sum_of_vals_from_A) / diag
if (lid == OUTPUT_THREAD)
{
#ifndef LDS_REDUCTION
// Wait for local memory to quiesce for the diagonal
// LDS_REDUCTION has such waits in it already.
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
#endif
FPTYPE out_val = temp_sum / diagonal[wf_id];
//FPTYPE out_val = temp_sum / diagonal[wf_id]; // original divide
out_y[row] = out_val;
}
}
total_rows_seen_so_far += totalRowsInEachLevel[current_level];
}
}
// Solves for 'y' in the equation 'A * y = alpha * x'
// This kernel is a simplified modification of the synchronization-free kernel.
// However, it is set up to work on rows that are in a contiguous series of
// levels. As such, this must be run after the initial analysis phase has
// produced a row map.
//
// Within a level, this kernel can use multiple workgroups to work on many
// rows simultaneously. In addition, multiple levels can be in flight at once,
// and this algorithm will use the synchronization-free spin-looping to produce
// the correct answer.
//
// However, we may not want to use *just* the synchronization-free spin-looping
// approach on all rows at the same time, as many rows deep in the dependency
// graph may just end up waiting, and spinning, for a long time. This spinning
// can slow down everyone else. As such, we partially break the dependency graph
// into multiple kernel invocations. This slightly reduces the theoretical
// parallelism, but it can make some invocations much faster due to less noise.
//
// The rowMap tells us that, within a level, thread X works on row Y.
// We need this because each level of the solve can have different numbers
// of non-contiguous row.
// In addition, the 'total_rows_in_prev_levels' tells us how far in that array
// to look, since previous kernel launches completed some previous rows.
__global__ void __launch_bounds__(WF_SIZE * WF_PER_WG, 1)
amd_spts_levelsync_solve(
size_t global_work_size,
const FPTYPE * __restrict__ vals,
const int * __restrict__ cols,
const int * __restrict__ rowPtrs,
const FPTYPE * __restrict__ vec_x,
FPTYPE * __restrict__ out_y,
const FPTYPE alpha,
unsigned int * __restrict__ doneArray,
const unsigned int * __restrict__ rowMap,
const unsigned int total_rows_in_prev_levels)
{
if (global_work_size <= hipBlockIdx_x * hipBlockDim_x + hipThreadIdx_x) return;
__shared__ FPTYPE *lds_ptr;
lds_ptr = nullptr;
#ifdef LDS_REDUCTION
__shared__ FPTYPE lds[WF_SIZE*WF_PER_WG];
lds_ptr = lds;
#endif
__shared__ FPTYPE diagonal[WF_PER_WG];
const unsigned int gid = hipBlockIdx_x;
const unsigned int wg_lid = hipThreadIdx_x;
const unsigned int lid = wg_lid % WF_SIZE;
const unsigned int wf_id = wg_lid / WF_SIZE;
const unsigned int row = rowMap[total_rows_in_prev_levels + (gid * WF_PER_WG) + wf_id];
FPTYPE temp_sum = 0.;
if (lid == OUTPUT_THREAD)
temp_sum = alpha * vec_x[row];
unsigned int start_of_this_row = rowPtrs[row];
unsigned int end_of_this_row = rowPtrs[row+1];
unsigned int start_point = start_of_this_row+lid;
// This wavefront operates on a single row, from its beginning to end.
for(unsigned int j = start_point; j < end_of_this_row; j+=WF_SIZE)
{
// local_col will tell us, for this iteration of the above for loop
// (i.e. for this entry in this row), which columns contain the
// non-zero values. We must then ensure that the output from the row
// associated with the local_col is complete to ensure that we can
// calculate the right answer.
int local_col = -1;
// Haul loading from vals[] up near the load of cols[] so that we get
// good coalsced loads.
FPTYPE local_val = 0.;
unsigned int local_done = 0;
// Replace the two loads below with inline assembly that sets the
// SLC bit. This forces the loads to essentially bypass the L2
// to increase cache hit rate on other instructions. Vals and cols
// are basically streamed in, so caching them doesn't help much.
// local_col = cols[j];
// local_val = vals[j];
#ifdef USE_DOUBLE
__asm__ volatile (MEM_PREFIX"_load_dword %0 %2 " OFF_MODIFIER " slc\n" MEM_PREFIX"_load_dwordx2 %1 %3 " OFF_MODIFIER " slc\ns_waitcnt vmcnt(0)" : "=v"(local_col), "=v"(local_val) : "v"(&cols[j]), "v"(&vals[j]));
#else
__asm__ volatile (MEM_PREFIX"_load_dword %0 %2 " OFF_MODIFIER " slc\n" MEM_PREFIX"_load_dword %1 %3 " OFF_MODIFIER " slc\ns_waitcnt vmcnt(0)" : "=v"(local_col), "=v"(local_val) : "v"(&cols[j]), "v"(&vals[j]));
#endif
// diagonal. Skip this, we need to solve for it.
if (local_col == row)
{
local_done = 1;
diagonal[wf_id] = local_val;
}
// While there are threads in this workgroup that have been unable to
// get their input, loop and wait for the flag to exist.
__asm__ volatile ("s_setprio 0");
while (!local_done)
{
{
// Replace this atomic with an assembly load with GLC bit set.
// This forces the load to go to the coherence point, allowing
// us to avoid deadlocks.
// local_done = atomic_get_done(doneArray, local_col);
__asm__ volatile (MEM_PREFIX"_load_dword %0 %1 " OFF_MODIFIER " glc slc\ns_waitcnt vmcnt(0)" : "=v"(local_done) : "v"(&doneArray[local_col]));
}
if (local_done)
{
FPTYPE out_val;
__asm__ volatile ("s_setprio 1");
// The command below is manually replaced with GCN assembly with
// the GLC bit set. This bypasses the L1, allowing us to do a
// coherent load of the variable without needing atomics.
#ifdef USE_DOUBLE
// out_val = as_double(atom_or((__global ulong *)&(out_y[local_col]), 0));
__asm__ volatile (MEM_PREFIX"_load_dwordx2 %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : "=v"(out_val) : "v"(&out_y[local_col]));
#else
// out_val = as_float(atomic_or((__global uint *)&(out_y[local_col]), 0));
__asm__ volatile (MEM_PREFIX"_load_dword %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : "=v"(out_val) : "v"(&out_y[local_col]));
#endif
temp_sum -= local_val * out_val;
}
}
}
__asm__ volatile ("s_setprio 1");
// Take all of the temp_sum values and add them together into
// OUTPUT_THREAD's temp_sum value.
temp_sum = cross_lane_reduction(temp_sum, lds_ptr, start_of_this_row,
end_of_this_row, wg_lid);
// y = (x-sum_of_vals_from_A) / diag
if (lid == OUTPUT_THREAD)
{
#ifndef LDS_REDUCTION
// Wait for local memory to quiesce for the diagonal
// LDS_REDUCTION has such waits in it already.
asm volatile ("s_waitcnt lgkmcnt(0)\n\t");
#endif
FPTYPE out_val = temp_sum / diagonal[wf_id];
//out_y[row] = out_val;
#ifdef USE_DOUBLE
__asm__ volatile (MEM_PREFIX"_store_dwordx2 %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : : "v" (&out_y[row]), "v"(out_val));
#else
__asm__ volatile (MEM_PREFIX"_store_dword %0 %1 " OFF_MODIFIER " glc\ns_waitcnt vmcnt(0)" : : "v" (&out_y[row]), "v"(out_val));
#endif
//out_y[row] = temp_sum / diagonal[wf_id]; // original divide
int set_one = 1;
//doneArray[row] = 1;
__asm__ volatile (MEM_PREFIX"_store_byte %0 %1 " OFF_MODIFIER " glc\n" WAKEUP : : "v"(&doneArray[row]), "v"(set_one));
// If you add this back in after doing a native_divide up above,
// we can get *some* of the accuracy of a full Newton-Raphson
// divide while maintaining the performance of the
// native_divide() on the critical path.
//out_y[row] = temp_sum / diagonal[wf_id];
}
}