Files
TheRock/rocm-libraries/projects/hipfft/shared/accuracy_test.h
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

831 lines
32 KiB
C++

// Copyright (C) 2020 - 2026 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.
#pragma once
#ifndef ACCURACY_TEST
#define ACCURACY_TEST
#include <algorithm>
#include <functional>
#include <future>
#include <gtest/gtest.h>
#include <iterator>
#include <string>
#include <vector>
#include "client_except.h"
#include "enum_to_string.h"
#include "fft_params.h"
#include "fftw_transform.h"
#include "gpubuf.h"
#include "reference_fft_data.h"
#include "rocfft_against_fftw.h"
#include "sys_mem.h"
#include "test_callbacks.h"
#include "test_params.h"
// Perform several checks to make sure buffers will fit in device memory
template <class Tparams>
inline void check_problem_fits_device_memory(Tparams& params, const int verbose)
{
int dev_id = hipInvalidDeviceId;
auto hip_status = hipGetDevice(&dev_id);
if(hip_status != hipSuccess || dev_id == hipInvalidDeviceId)
{
throw hip_runtime_error("hipGetDevice failed", hip_status);
}
const auto vram_avail = device_memory_accountant::singleton().get_usable_bytes_all_devices();
// First try a quick estimation of vram footprint, to speed up skipping tests
// that are too large to fit in the gpu (no plan created with the rocFFT backend)
const auto io_vram_footprint = params.io_vram_footprint();
if(!vram_fits_problem(io_vram_footprint, vram_avail))
{
std::stringstream ss;
ss << "Raw problem size (" << byte_sizes_to_str(io_vram_footprint)
<< ") exceeds usable memory on some device (" << byte_sizes_to_str(vram_avail) << ")";
throw ROCFFT_SKIP{ss.str()};
}
if(verbose > 2)
{
std::cout << "Raw problem size: " << byte_sizes_to_str(io_vram_footprint) << std::endl;
}
// If it passed the quick estimation test, go for the more
// accurate calculation that actually creates the plan and
// take into account the work buffer size
const auto vram_footprint = params.vram_footprint();
if(!vram_fits_problem(vram_footprint, vram_avail))
{
if(verbose)
{
std::cout << "Problem raw data won't fit on device; skipped." << std::endl;
}
std::stringstream ss;
ss << "Problem size (" << byte_sizes_to_str(vram_footprint)
<< ") exceeds usable memory on some device (" << byte_sizes_to_str(vram_avail) << ")";
throw ROCFFT_SKIP{ss.str()};
}
}
template <typename Tfloat>
bool fftw_plan_uses_bluestein(const fftw_plan_wrapper_t<Tfloat>& cpu_plan)
{
#ifdef FFTW_HAVE_SPRINT_PLAN
char* print_plan_c_str = fftw_sprint_plan<Tfloat>(cpu_plan);
std::string print_plan(print_plan_c_str);
free(print_plan_c_str);
return print_plan.find("bluestein") != std::string::npos;
#else
// assume worst case (bluestein is always used)
return true;
#endif
}
// Base gtest class for comparison with FFTW.
class accuracy_test : public ::testing::TestWithParam<fft_params>
{
protected:
void SetUp() override {}
void TearDown() override {}
public:
static std::string TestName(const testing::TestParamInfo<accuracy_test::ParamType>& info)
{
return info.param.token();
}
};
// execute the GPU transform
template <class Tparams>
inline void execute_gpu_fft(Tparams& params,
std::vector<void*>& pibuffer,
std::vector<void*>& pobuffer,
std::vector<gpubuf>& obuffer,
std::vector<hostbuf>& gpu_output,
bool round_trip_inverse = false)
{
// Vector of callback data - at function scope so they live until
// after the transform is completed
std::vector<gpubuf_t<callback_test_data>> all_cb_data;
std::vector<void*> load_cb_func;
std::vector<void*> load_cb_data;
std::vector<void*> store_cb_func;
std::vector<void*> store_cb_data;
// Function pointer callbacks are provided at execution time
if(params.run_callbacks == fft_callback_type_funcptr)
{
get_rank_load_callbacks_funcptr(
params, load_cb_func, load_cb_data, round_trip_inverse, all_cb_data);
get_rank_store_callbacks_funcptr(
params, store_cb_func, store_cb_data, round_trip_inverse, all_cb_data);
auto fft_status = params.set_funcptr_callbacks(
&load_cb_func, &load_cb_data, &store_cb_func, &store_cb_data);
if(fft_status != fft_status_success)
throw std::runtime_error("set callback failure");
}
// Execute the transform:
auto fft_status = params.execute(pibuffer.data(), pobuffer.data());
if(fft_status != fft_status_success)
throw std::runtime_error("FFT plan execution failure");
// if not comparing, then just executing the GPU FFT is all we
// need to do
if(!fftw_compare)
return;
ASSERT_TRUE(!gpu_output.empty()) << "no output buffers";
// if output is in multiple bricks, collect it into the
// host buffer where the results need to go
params.multi_gpu_finalize(gpu_output, obuffer, pobuffer);
if(params.ofields.empty())
{
// otherwise, copy directly from the device
for(unsigned int idx = 0; idx < gpu_output.size(); ++idx)
{
ASSERT_TRUE(gpu_output[idx].data() != nullptr)
<< "output buffer index " << idx << " is empty";
auto hip_status = hipMemcpy(gpu_output[idx].data(),
pobuffer.at(idx),
gpu_output[idx].size(),
hipMemcpyDeviceToHost);
if(hip_status != hipSuccess)
{
throw hip_runtime_error("hipMemcpy failure", hip_status);
}
}
}
if(verbose > 2)
{
std::cout << "GPU output:\n";
params.print_obuffer(gpu_output);
}
if(verbose > 5)
{
std::cout << "flat GPU output:\n";
params.print_obuffer_flat(gpu_output);
}
}
template <typename Tfloat>
static void assert_init_value(const std::vector<hostbuf>& output,
const size_t idx,
const Tfloat orig_value);
template <>
void assert_init_value(const std::vector<hostbuf>& output, const size_t idx, const float orig_value)
{
float actual_value = reinterpret_cast<const float*>(output.front().data())[idx];
ASSERT_EQ(actual_value, orig_value) << "index " << idx;
}
template <>
void assert_init_value(const std::vector<hostbuf>& output,
const size_t idx,
const double orig_value)
{
double actual_value = reinterpret_cast<const double*>(output.front().data())[idx];
ASSERT_EQ(actual_value, orig_value) << "index " << idx;
}
template <>
void assert_init_value(const std::vector<hostbuf>& output,
const size_t idx,
const rocfft_complex<float> orig_value)
{
// if this is interleaved, check directly
if(output.size() == 1)
{
rocfft_complex<float> actual_value
= reinterpret_cast<const rocfft_complex<float>*>(output.front().data())[idx];
ASSERT_EQ(actual_value.x, orig_value.x) << "x index " << idx;
ASSERT_EQ(actual_value.y, orig_value.y) << "y index " << idx;
}
else
{
// planar
rocfft_complex<float> actual_value{
reinterpret_cast<const float*>(output.front().data())[idx],
reinterpret_cast<const float*>(output.back().data())[idx]};
ASSERT_EQ(actual_value.x, orig_value.x) << "x index " << idx;
ASSERT_EQ(actual_value.y, orig_value.y) << "y index " << idx;
}
}
template <>
void assert_init_value(const std::vector<hostbuf>& output,
const size_t idx,
const rocfft_complex<double> orig_value)
{
// if this is interleaved, check directly
if(output.size() == 1)
{
rocfft_complex<double> actual_value
= reinterpret_cast<const rocfft_complex<double>*>(output.front().data())[idx];
ASSERT_EQ(actual_value.x, orig_value.x) << "x index " << idx;
ASSERT_EQ(actual_value.y, orig_value.y) << "y index " << idx;
}
else
{
// planar
rocfft_complex<double> actual_value{
reinterpret_cast<const double*>(output.front().data())[idx],
reinterpret_cast<const double*>(output.back().data())[idx]};
ASSERT_EQ(actual_value.x, orig_value.x) << "x index " << idx;
ASSERT_EQ(actual_value.y, orig_value.y) << "y index " << idx;
}
}
static const int OUTPUT_INIT_PATTERN = 0xcd;
template <class Tfloat>
void check_single_output_stride(const std::vector<hostbuf>& output,
const size_t offset,
const std::vector<size_t>& length,
const std::vector<size_t>& stride,
const size_t i)
{
Tfloat orig;
memset(static_cast<void*>(&orig), OUTPUT_INIT_PATTERN, sizeof(Tfloat));
size_t curLength = length[i];
size_t curStride = stride[i];
size_t nextSmallerLength = i == length.size() - 1 ? 0 : length[i + 1];
size_t nextSmallerStride = i == stride.size() - 1 ? 0 : stride[i + 1];
if(nextSmallerLength == 0)
{
// this is the fastest dim, indexes that are not multiples of
// the stride should be the initial value
for(size_t idx = 0; idx < (curLength - 1) * curStride; ++idx)
{
if(idx % curStride != 0)
assert_init_value<Tfloat>(output, idx, orig);
}
}
else
{
for(size_t lengthIdx = 0; lengthIdx < curLength; ++lengthIdx)
{
// check that the space after the next smaller dim and the
// end of this dim is initial value
for(size_t idx = nextSmallerLength * nextSmallerStride; idx < curStride; ++idx)
assert_init_value<Tfloat>(output, idx, orig);
check_single_output_stride<Tfloat>(
output, offset + lengthIdx * curStride, length, stride, i + 1);
}
}
}
template <class Tparams>
void check_output_strides(const std::vector<hostbuf>& output, Tparams& params)
{
// treat batch+dist like highest length+stride, if batch > 1
std::vector<size_t> length;
std::vector<size_t> stride;
if(params.nbatch > 1)
{
length.push_back(params.nbatch);
stride.push_back(params.odist);
}
auto olength = params.olength();
std::copy(olength.begin(), olength.end(), std::back_inserter(length));
std::copy(params.ostride.begin(), params.ostride.end(), std::back_inserter(stride));
if(params.precision == fft_precision_single)
{
if(params.otype == fft_array_type_real)
check_single_output_stride<float>(output, 0, length, stride, 0);
else
check_single_output_stride<rocfft_complex<float>>(output, 0, length, stride, 0);
}
else
{
if(params.otype == fft_array_type_real)
check_single_output_stride<double>(output, 0, length, stride, 0);
else
check_single_output_stride<rocfft_complex<double>>(output, 0, length, stride, 0);
}
}
// run rocFFT inverse transform
template <class Tparams>
inline void run_round_trip_inverse(Tparams& params,
std::vector<gpubuf>& obuffer,
std::vector<void*>& pibuffer,
std::vector<void*>& pobuffer,
std::vector<hostbuf>& gpu_output)
{
if(params.run_callbacks == fft_callback_type_jit)
{
params.load_jit_cb_state = get_rank_jit_state(
params, "load_callback_round_trip_inverse", true, jit_callback_op::LOAD);
params.store_jit_cb_state = get_rank_jit_state(
params, "store_callback_round_trip_inverse", true, jit_callback_op::STORE);
}
params.validate();
// Make sure that the parameters make sense:
ASSERT_TRUE(params.valid(verbose));
// Create FFT plan - this will also allocate work buffer, but will throw a
// specific exception if that step fails
auto plan_status = fft_status_success;
plan_status = params.create_plan();
ASSERT_EQ(plan_status, fft_status_success) << "round trip inverse plan creation failed";
auto obuffer_sizes = params.obuffer_sizes();
if(params.placement != fft_placement_inplace)
{
for(unsigned int i = 0; i < obuffer_sizes.size(); ++i)
{
// If we're validating output strides, init the
// output buffer to a known pattern and we can check
// that the pattern is untouched in places that
// shouldn't have been touched.
if(params.check_output_strides)
{
auto hip_status = hipMemset(pobuffer[i], OUTPUT_INIT_PATTERN, obuffer_sizes[i]);
if(hip_status != hipSuccess)
{
throw hip_runtime_error("hipMemset failure", hip_status);
}
}
}
}
if(params.multiGPU > 1)
{
if(verbose > 0)
{
std::cout << "scattering data for multi-GPU inverse" << std::endl;
}
std::vector<hostbuf> cpu_input;
params.multi_gpu_prepare(cpu_input, obuffer, pibuffer, pobuffer);
}
// execute GPU transform
execute_gpu_fft(params, pibuffer, pobuffer, obuffer, gpu_output, true);
}
// compare rocFFT inverse transform with forward transform input
template <class Tparams>
inline void compare_round_trip_inverse(Tparams& params,
const fft_params& contiguous_params,
std::vector<hostbuf>& gpu_output,
const std::vector<hostbuf>& cpu_input,
const VectorNorms& cpu_input_norm,
size_t total_length)
{
if(params.check_output_strides)
{
check_output_strides<Tparams>(gpu_output, params);
}
// compute GPU output norm
std::shared_future<VectorNorms> gpu_norm = std::async(std::launch::async, [&]() {
return norm(gpu_output,
params.olength(),
params.nbatch,
params.precision,
params.otype,
params.ostride,
params.odist,
params.ooffset);
});
// compare GPU inverse output to CPU forward input
std::unique_ptr<std::vector<std::pair<size_t, size_t>>> linf_failures;
if(verbose > 1)
linf_failures = std::make_unique<std::vector<std::pair<size_t, size_t>>>();
const double linf_cutoff
= type_epsilon(params.precision) * cpu_input_norm.l_inf * log(total_length);
VectorNorms diff = distance(cpu_input,
gpu_output,
params.olength(),
params.nbatch,
params.precision,
contiguous_params.itype,
contiguous_params.istride,
contiguous_params.idist,
params.otype,
params.ostride,
params.odist,
linf_failures.get(),
linf_cutoff,
{0},
params.ooffset,
1.0 / total_length);
if(verbose > 1)
{
std::cout << "GPU output Linf norm: " << gpu_norm.get().l_inf << "\n";
std::cout << "GPU output L2 norm: " << gpu_norm.get().l_2 << "\n";
std::cout << "GPU linf norm failures:";
std::sort(linf_failures->begin(), linf_failures->end());
for(const auto& i : *linf_failures)
{
std::cout << " (" << i.first << "," << i.second << ")";
}
std::cout << std::endl;
}
EXPECT_TRUE(std::isfinite(gpu_norm.get().l_inf)) << params.str();
EXPECT_TRUE(std::isfinite(gpu_norm.get().l_2)) << params.str();
switch(params.precision)
{
case fft_precision_half:
max_linf_eps_half
= std::max(max_linf_eps_half, diff.l_inf / cpu_input_norm.l_inf / log(total_length));
max_l2_eps_half
= std::max(max_l2_eps_half, diff.l_2 / cpu_input_norm.l_2 * sqrt(log2(total_length)));
break;
case fft_precision_single:
max_linf_eps_single
= std::max(max_linf_eps_single, diff.l_inf / cpu_input_norm.l_inf / log(total_length));
max_l2_eps_single
= std::max(max_l2_eps_single, diff.l_2 / cpu_input_norm.l_2 * sqrt(log2(total_length)));
break;
case fft_precision_double:
max_linf_eps_double
= std::max(max_linf_eps_double, diff.l_inf / cpu_input_norm.l_inf / log(total_length));
max_l2_eps_double
= std::max(max_l2_eps_double, diff.l_2 / cpu_input_norm.l_2 * sqrt(log2(total_length)));
break;
}
if(verbose > 1)
{
std::cout << "L2 diff: " << diff.l_2 << "\n";
std::cout << "Linf diff: " << diff.l_inf << "\n";
}
EXPECT_TRUE(diff.l_inf <= linf_cutoff)
<< "Linf test failed. Linf:" << diff.l_inf
<< "\tnormalized Linf: " << diff.l_inf / cpu_input_norm.l_inf << "\tcutoff: " << linf_cutoff
<< params.str();
EXPECT_TRUE(diff.l_2 / cpu_input_norm.l_2
<= sqrt(log2(total_length)) * type_epsilon(params.precision))
<< "L2 test failed. L2: " << diff.l_2
<< "\tnormalized L2: " << diff.l_2 / cpu_input_norm.l_2
<< "\tepsilon: " << sqrt(log2(total_length)) * type_epsilon(params.precision)
<< params.str();
}
// run CPU + rocFFT transform with the given params and compare
template <class Tfloat, class Tparams>
inline void fft_vs_reference_impl(Tparams& params, bool round_trip)
{
// Call hipGetLastError to reset any errors
// returned by previous HIP runtime API calls.
hipError_t hip_status = hipGetLastError();
if(params.run_callbacks == fft_callback_type_jit)
{
params.load_jit_cb_state
= get_rank_jit_state(params, "load_callback", false, jit_callback_op::LOAD);
params.store_jit_cb_state
= get_rank_jit_state(params, "store_callback", false, jit_callback_op::STORE);
}
// Make sure that the parameters make sense:
ASSERT_TRUE(params.valid(verbose));
// TODO: temporary workaround awaiting robust support for
// 64-bit indexing in rocfft kernels.
if(params.may_need_64bit_indexing())
throw ROCFFT_SKIP{"This test may require kernel support for 64-bit integer arithmetic"};
// Create reference results as early as possible so that system memory is reserved for (an
// estimation of) the possible FFTW plan's workspace (if needed), providing some guard against
// OOM kills thereafter.
// If reference results need to be computed, computation should be launched as soon as possible.
// This helps with respect to management of system-memory usage due to
// - more reliable accounting: once the FFTW plan's workspace is actually used, that may trigger
// a difference in the system's reported free memory (if not reported already) which better
// reflects its actual size. This is most relevant to better guard against OOM kills, if our
// estimated reservation was too optimistic (too small);
// - releasing system memory related to the needs for computing reference results as soon as
// possible, alleviating pressure for subsequent operations.
// TODO: make this object an std::optional<reference_fft_data_t>, set only when `fftw_compare`
// is true when cpu data sets are no longer needed for unrelated aspects.
reference_fft_data_t reference_results{params.make_params_for_reference_cpu()};
if(fftw_compare)
{
if(reference_results.needs_input_initialization() && is_host_generator(params.igen))
reference_results.initialize_input(params.igen);
if(reference_results.needs_computing() && !reference_results.needs_input_initialization())
reference_results.launch_async_compute();
}
// Make sure FFT buffers fit in device memory
check_problem_fits_device_memory(params, verbose);
auto ibuffer_sizes = params.ibuffer_sizes();
auto obuffer_sizes = params.obuffer_sizes();
// Allocate device input buffer(s)
std::vector<gpubuf> ibuffer(ibuffer_sizes.size());
std::vector<void*> pibuffer(ibuffer_sizes.size());
for(unsigned int i = 0; i < ibuffer.size(); ++i)
{
hip_status = ibuffer[i].alloc(ibuffer_sizes[i]);
if(hip_status != hipSuccess)
{
std::stringstream ss;
if(hip_status == hipErrorOutOfMemory)
{
ss << "Input buffer size (" << byte_size_to_str(ibuffer_sizes[i])
<< ") raw data too large for device";
}
else
{
ss << "hipMalloc failure for input buffer " << i
<< " (size: " << byte_size_to_str(ibuffer_sizes[i]) << ") with code "
<< hipError_to_string(hip_status);
}
throw hip_runtime_error(ss.str(), hip_status);
}
pibuffer[i] = ibuffer[i].data();
}
// Initialize input data buffer(s) on device
if(!reference_results.needs_input_initialization() || is_host_generator(params.igen))
{
if(reference_results.needs_input_initialization())
{
reference_results.initialize_input(params.igen);
if(fftw_compare && reference_results.needs_computing())
reference_results.launch_async_compute();
}
reference_results.copy_input_data_in_device_buffers(ibuffer, params);
}
else
{
params.compute_input(ibuffer);
reference_results.initialize_input_using(ibuffer, params);
}
if(fftw_compare && reference_results.needs_computing())
reference_results.launch_async_compute();
// Create FFT plan - this will also allocate work buffer, but
// will throw a specific exception if that step fails
auto plan_status = fft_status_success;
plan_status = params.create_plan();
ASSERT_EQ(plan_status, fft_status_success) << "plan creation failed";
if(verbose > 3)
reference_results.print_data<fft_io::fft_io_in>();
// compute input norm
auto cpu_input_norm = reference_results.get_norm<fft_io::fft_io_in>(params.nbatch);
std::vector<gpubuf> obuffer_data;
std::vector<gpubuf>* obuffer = &obuffer_data;
std::vector<void*> pobuffer;
// allocate the output buffer
if(params.placement == fft_placement_inplace)
{
obuffer = &ibuffer;
}
else
{
auto obuffer_sizes = params.obuffer_sizes();
obuffer_data.resize(obuffer_sizes.size());
for(unsigned int i = 0; i < obuffer_data.size(); ++i)
{
hip_status = obuffer_data[i].alloc(obuffer_sizes[i]);
if(hip_status != hipSuccess)
{
std::stringstream ss;
ss << "hipMalloc failure for output buffer " << i
<< " (size: " << byte_size_to_str(obuffer_sizes[i]) << ") with code "
<< hipError_to_string(hip_status);
throw hip_runtime_error(ss.str(), hip_status);
}
// If we're validating output strides, init the
// output buffer to a known pattern and we can check
// that the pattern is untouched in places that
// shouldn't have been touched.
if(params.check_output_strides)
{
hip_status
= hipMemset(obuffer_data[i].data(), OUTPUT_INIT_PATTERN, obuffer_sizes[i]);
if(hip_status != hipSuccess)
{
std::stringstream ss;
ss << "hipMemset failure with error " << hip_status;
throw hip_runtime_error(ss.str(), hip_status);
}
}
}
}
pobuffer.resize(obuffer->size());
for(unsigned int i = 0; i < obuffer->size(); ++i)
{
pobuffer[i] = obuffer->at(i).data();
}
// scatter data out to multi-GPUs if this is a multi-GPU test
params.multi_gpu_prepare(
reference_results.get_buffers<fft_io::fft_io_in>(), ibuffer, pibuffer, pobuffer);
std::shared_future<VectorNorms> cpu_output_norm;
if(fftw_compare)
{
if(verbose > 3)
reference_results.print_data<fft_io::fft_io_out>();
cpu_output_norm = reference_results.get_norm<fft_io::fft_io_out>(params.nbatch);
}
// execute GPU transform
std::vector<hostbuf> gpu_output
= allocate_host_buffer(params.precision, params.otype, params.osize);
execute_gpu_fft(params, pibuffer, pobuffer, *obuffer, gpu_output);
params.free();
if(params.check_output_strides)
{
check_output_strides<Tparams>(gpu_output, params);
}
// compute GPU output norm
std::shared_future<VectorNorms> gpu_norm;
if(fftw_compare)
gpu_norm = std::async(std::launch::async, [&]() {
return norm(gpu_output,
params.olength(),
params.nbatch,
params.precision,
params.otype,
params.ostride,
params.odist,
params.ooffset);
});
// compare output
//
// Compute the l-infinity and l-2 distance between the CPU and GPU output:
// wait for cpu FFT so we can compute cutoff
const auto total_length = product(params.length.begin(), params.length.end());
std::unique_ptr<std::vector<std::pair<size_t, size_t>>> linf_failures;
if(verbose > 1)
linf_failures = std::make_unique<std::vector<std::pair<size_t, size_t>>>();
double linf_cutoff;
VectorNorms diff;
std::shared_future<void> compare_output;
if(fftw_compare)
compare_output = std::async(std::launch::async, [&]() {
linf_cutoff
= type_epsilon(params.precision) * cpu_output_norm.get().l_inf * log(total_length);
diff = distance(reference_results.get_buffers<fft_io::fft_io_out>(),
gpu_output,
params.olength(),
params.nbatch,
params.precision,
reference_results.get_params().otype,
reference_results.get_params().ostride,
reference_results.get_params().odist,
params.otype,
params.ostride,
params.odist,
linf_failures.get(),
linf_cutoff,
{0},
params.ooffset);
});
if(compare_output.valid())
compare_output.get();
Tparams params_inverse;
std::vector<hostbuf> roundtrip_output_buffers;
if(round_trip)
{
params_inverse.inverse_from_forward(params);
params_inverse.compute_osize();
roundtrip_output_buffers = allocate_host_buffer(params_inverse.obuffer_sizes());
run_round_trip_inverse<Tparams>(
params_inverse, *obuffer, pobuffer, pibuffer, roundtrip_output_buffers);
}
if(fftw_compare)
{
ASSERT_TRUE(std::isfinite(cpu_input_norm.get().l_2));
ASSERT_TRUE(std::isfinite(cpu_input_norm.get().l_inf));
ASSERT_TRUE(std::isfinite(cpu_output_norm.get().l_2));
ASSERT_TRUE(std::isfinite(cpu_output_norm.get().l_inf));
if(verbose > 1)
{
std::cout << "GPU output Linf norm: " << gpu_norm.get().l_inf << "\n";
std::cout << "GPU output L2 norm: " << gpu_norm.get().l_2 << "\n";
std::cout << "GPU linf norm failures:";
std::sort(linf_failures->begin(), linf_failures->end());
for(const auto& i : *linf_failures)
{
std::cout << " (" << i.first << "," << i.second << ")";
}
std::cout << std::endl;
}
EXPECT_TRUE(std::isfinite(gpu_norm.get().l_inf)) << params.str();
EXPECT_TRUE(std::isfinite(gpu_norm.get().l_2)) << params.str();
switch(params.precision)
{
case fft_precision_half:
max_linf_eps_half = std::max(
max_linf_eps_half, diff.l_inf / cpu_output_norm.get().l_inf / log(total_length));
max_l2_eps_half = std::max(
max_l2_eps_half, diff.l_2 / cpu_output_norm.get().l_2 * sqrt(log2(total_length)));
break;
case fft_precision_single:
max_linf_eps_single = std::max(
max_linf_eps_single, diff.l_inf / cpu_output_norm.get().l_inf / log(total_length));
max_l2_eps_single = std::max(
max_l2_eps_single, diff.l_2 / cpu_output_norm.get().l_2 * sqrt(log2(total_length)));
break;
case fft_precision_double:
max_linf_eps_double = std::max(
max_linf_eps_double, diff.l_inf / cpu_output_norm.get().l_inf / log(total_length));
max_l2_eps_double = std::max(
max_l2_eps_double, diff.l_2 / cpu_output_norm.get().l_2 * sqrt(log2(total_length)));
break;
}
if(verbose > 1)
{
std::cout << "L2 diff: " << diff.l_2 << "\n";
std::cout << "Linf diff: " << diff.l_inf << "\n";
}
EXPECT_TRUE(diff.l_inf <= linf_cutoff)
<< "Linf test failed. Linf:" << diff.l_inf
<< "\tnormalized Linf: " << diff.l_inf / cpu_output_norm.get().l_inf
<< "\tcutoff: " << linf_cutoff << "\n"
<< params.str();
EXPECT_TRUE(diff.l_2 / cpu_output_norm.get().l_2
<= sqrt(log2(total_length)) * type_epsilon(params.precision))
<< "L2 test failed. L2: " << diff.l_2
<< "\tnormalized L2: " << diff.l_2 / cpu_output_norm.get().l_2
<< "\tepsilon: " << sqrt(log2(total_length)) * type_epsilon(params.precision) << "\n"
<< params.str();
if(round_trip)
{
compare_round_trip_inverse<Tparams>(params_inverse,
reference_results.get_params(),
roundtrip_output_buffers,
reference_results.get_buffers<fft_io::fft_io_in>(),
cpu_input_norm.get(),
total_length);
}
}
}
#endif