在CUDA中,如何高效地判断一个半精度浮点数是否有限?
CUDA提供一个(NVIDIA GPU)函数,用于检查半精度值是否为无穷大: __hisinf()。但——没有对应的 __hisfinite() 函数(就像用于双精度值的函数,确实存在:int isfinite(double x))。
一个 bool isfinite(half x) 函数的最有效实现会是什么?具体来说,
- 我是不是就用
not __hisinf(x) and not __hisnan(x),希望编译器能把它优化得足够好? - 我是否需要进行位运算,确保指数部分不是全为1?
- 还有其他办法吗?
“Efficient” 并不是一个精确的术语,原理上可能存在延迟和吞吐量的权衡,但我猜在这种情况下,这并不是问题。
解决方案
(在 njuffa 的回答之后编辑。)
tl;dr: 最快的方法似乎是直接在半精度值的 unsigned short 表示中,将指数位与全1 的模式进行比较。但——这个结果可能取决于你如何衡量。
需要考虑的多种方法
到目前为止,在我们这些人之间,似乎有6 种建议的方法来检查半精度值的有限性/有限性:
- (来自 @njuffa)按IEEE 754,当且仅当与正无穷相比为更小的数才是有限的。现在只需要得到正无穷的值——我们可以通过从已知的整数表示得到
__ushort2half(),或者通过对INFINITY宏进行简单的强制类型转换来获得:
bool isfinite(half x) { return __habs(x) < ((half)INFINITY); }
2. (来自 @njuffa)CUDA提供用于检查 float 值有限性的内建函数;因此我们直接把它强制转换为 float 并使用它。
bool isfinite(half x) { return isfinite(__half2float(x)); }
3. (来自 @wim)如果把表示的最低15位(即忽略符号)作为一个整数值来考虑,那么其中的前5 位需要全部为1;因此有限数的值要小于非有限数。
bool isfinite(half x) {
unsigned short shifted_plus_infinity_rep = 0b0111110000000000u << 1;
unsigned short shifted_input_rep = __half_as_ushort (x) << 1;
return shifted_input_rep < shifted_plus_infinity_rep;
}
4.直接检查指数位:非有限值的指数为全1;有限值则是其他值。
bool isfinite(half x){
auto exponent_bits_mask = 0b111110000000000u;
return (__half_as_ushort(x) & exponent_bits_mask) != exponent_bits_mask;
}
5.通过位域检查指数位(本质上与前一种方法相同)
bool isfinite(half x){
union destructured_half {
half value;
struct {
unsigned short mantissa: 10;
unsigned short exponent : 5;
unsigned short sign : 1;
} components;
};
destructured_half d { .value = x };
return d.components.exponent != 0b11111u; // all ones
}
6. (@ericpostpischil的想法)用自我相减。只有有限值在与自身相减时才会得到 0(对于无穷大和NaN,结果是NaN)。这种方法在代码上最简短,具有美学吸引力。
bool isfinite(half x){
return x - x == (half) 0;
}
编译结果分析
改编自 这里(GodBolt.org,CUDA 13.2,SM 89目标)。
前置、签名和最终存储指令省略。加载指令本应是通用的——但实际上并非如此!因此只去除了通用的 R1 设置。
方法1:
LDC.U16 R0, c[0x0][0x168]
MOV R2, c[0x0][0x160]
IMAD.MOV.U32 R3, RZ, RZ, c[0x0][0x164]
ULDC.64 UR4, c[0x0][0x118]
HSETP2.GEU.AND P0, PT, |R0|.H0_H0, +INF , +INF , PT
SEL R5, RZ, 0x1, P0
方法2:
HADD2.F32 R0, -RZ, c[0x0] [0x168].H0_H0
MOV R2, c[0x0][0x160]
IMAD.MOV.U32 R3, RZ, RZ, c[0x0][0x164]
ULDC.64 UR4, c[0x0][0x118]
FSETP.GEU.AND P0, PT, |R0|, +INF , PT
SEL R5, RZ, 0x1, P0
方法3:
LDC.U16 R0, c[0x0][0x168]
MOV R2, c[0x0][0x160]
IMAD.MOV.U32 R3, RZ, RZ, c[0x0][0x164]
ULDC.64 UR4, c[0x0][0x118]
IMAD.SHL.U32 R0, R0, 0x2, RZ
LOP3.LUT R0, R0, 0xffff, RZ, 0xc0, !PT
ISETP.GE.U32.AND P0, PT, R0, 0xf800, PT
SEL R5, RZ, 0x1, P0
方法4:
LDC.U16 R0, c[0x0][0x168]
IMAD.MOV.U32 R2, RZ, RZ, c[0x0][0x160]
ULDC.64 UR4, c[0x0][0x118]
IMAD.MOV.U32 R3, RZ, RZ, c[0x0][0x164]
LOP3.LUT R0, R0, 0x7c00, RZ, 0xc0, !PT
ISETP.NE.AND P0, PT, R0, 0x7c00, PT
SEL R5, RZ, 0x1, !P0
方法5:
LDC.U16 R0, c[0x0][0x168]
IMAD.MOV.U32 R2, RZ, RZ, c[0x0][0x160]
ULDC.64 UR4, c[0x0][0x118]
IMAD.MOV.U32 R3, RZ, RZ, c[0x0][0x164]
LOP3.LUT R0, R0, 0x7c00, RZ, 0xc0, !PT
ISETP.NE.AND P0, PT, R0, 0x7c00, PT
SEL R5, RZ, 0x1, !P0
方法6:
LDC.U16 R0, c[0x0][0x168]
MOV R2, c[0x0][0x160]
IMAD.MOV.U32 R3, RZ, RZ, c[0x0][0x164]
ULDC.64 UR4, c[0x0][0x118]
HADD2 R0, R0.H0_H0, -c[0x0] [0x168].H0_H0
HSETP2.NEU.AND P0, PT, R0.H0_H0, RZ.H0_H0, PT
SEL R5, RZ, 0x1, P0
方法2 中的 HADD2.F32 非常奇怪!它把半精度值加到负零上。为什么?
性能比较
基准测试方法
我采用了 @njuffa的微基准内核写法:遍历所有 unsigned short 值,将每个值视为半精度值,并检查其是否有限。不同的线程从不同的 unsigned short 值开始。内核被调度多次。仅运行一个相同的块网格尺寸、但不包含有限性检查逻辑的内核开销可以忽略不计——只有几微秒;因此忽略它的影响。
不过我用的计时方式与 @njuffa不同:
- 使用CUDA事件而不是系统时间。我也使用了cuda-api-wrappers。
- 取内核调用的中位数而不是最小值,重复10次。
- 运行100次迭代
(如果你认为这些选择有问题,请在评论中说明。)
结果
在NVIDIA RTX 4600上:
| 序号 | 方法名称 | 时间(毫秒) |
|---|---|---|
| 1 | 小于无穷大字面量 | 6.7031 |
| 2 | 使用float类型的CUDA内置函数 | 7.25789 |
| 3 | 小于无符号短整型阈值值 | 8.26982 |
| 4 | 直接比较指数位 | 6.56589 |
| 5 | 通过结构化解构比较指数位 | 6.56835 |
| 6 | 自我相减判定法 | 7.25725 |
看起来获胜的方法是将指数位与无穷大与NaN的模式进行对比。没有明显差异
我很快会在其他GPU上也试试,敬请关注...
基准测试程序:
#include <cuda/api.hpp>
#include <cuda_fp16.h>
#include <iostream>
#include <iomanip>
enum FinityCheckMethod {
smaller_than_infinity_literal, // Method 1
using_cuda_intrinsic_for_float_type, // Method 2
less_than_ushort_threshold_value_for_non_finite, // Method 3
bit_pattern_comparison, // Method 4
bit_pattern_comparison_via_destructuring, // Method 5
self_subtraction, // Method 6
};
#define __fd__ __forceinline__ __device__
template <FinityCheckMethod FCM>
__fd__ bool isfinite (half x);
template <> __fd__ bool isfinite<smaller_than_infinity_literal>(half x)
{
return __habs(x) < ((half)INFINITY);
}
template <> __fd__ bool isfinite<using_cuda_intrinsic_for_float_type>(half x)
{
return isfinite(__half2float(x));
}
template <> __fd__ bool isfinite<less_than_ushort_threshold_value_for_non_finite>(half x)
{
static constexpr unsigned short shifted_plus_infinity_rep = 0b0111110000000000u << 1;
return (unsigned short)(__half_as_ushort (x) << 1) < shifted_plus_infinity_rep;
}
template <> __fd__ bool isfinite<bit_pattern_comparison>(half x)
{
auto exponent_bits_mask = 0b0111110000000000u; // 10 least-significant bits are the mantissa
return (__half_as_ushort(x) & exponent_bits_mask) != exponent_bits_mask;
}
template <> __fd__ bool isfinite<bit_pattern_comparison_via_destructuring>(half x)
{
union destructured_half {
half value;
struct {
unsigned short mantissa: 10;
unsigned short exponent : 5;
unsigned short sign : 1;
} components;
};
destructured_half d { .value = x };
return d.components.exponent != 0b11111u; // all ones
}
template <> __fd__ bool isfinite<self_subtraction>(half x)
{
return x - x == (half) 0;
}
using count_t = unsigned int;
template <FinityCheckMethod FCM>
__global__ void kernel(count_t *finity_counts)
{
auto tid = blockDim.x * blockIdx.x + threadIdx.x;
unsigned short arg = tid;
count_t res = 0;
// one iteration for every one of the 2^16 possible ushort values
do {
res = res + isfinite<FCM>(__ushort_as_half(arg));
arg++;
} while (arg != tid);
finity_counts[tid] = res;
}
const char* name_of(FinityCheckMethod FCM)
{
static constexpr const char* names[] = {
"smaller than infinity literal",
"using cuda intrinsic for float type",
"less than ushort threshold value for non-finite",
"direct exponent bits comparison",
"exponent bits comparison via destructuring",
"self-substraction criterion",
};
static constexpr auto num_names = sizeof(names)/sizeof(char const*);
return (FCM >= num_names) ? nullptr : names[FCM];
}
template <FinityCheckMethod FCM>
void sanity_check(cuda::span<count_t> d_finity_counts, cuda::span<count_t> finity_counts)
{
for (size_t tid = 0; tid < d_finity_counts.size(); tid++) {
auto num_encodings_of_finite_half_values { 0xf800u };
if (finity_counts[tid] != num_encodings_of_finite_half_values) {
std::cout
<< "sanity check for check variant " << name_of(FCM) << " failed: "
<< "Thread " << tid << "'s counted " << finity_counts[tid]
<< " finite representations rather than " << num_encodings_of_finite_half_values;
exit(EXIT_FAILURE);
}
}
}
template <FinityCheckMethod FCM>
void time_method(cuda::device_t const& device)
{
auto threads_per_block { 128 };
auto num_ushort_values { 1lu << sizeof(unsigned short) * CHAR_BIT };
assert(num_ushort_values % threads_per_block == 0);
auto num_iterations { 100 };
auto launch_config = cuda::launch_config_builder()
.overall_size(num_ushort_values)
.block_size(threads_per_block)
.build();
auto total_threads = num_ushort_values;
auto d_finity_counts = cuda::memory::make_unique_span<count_t>(device, total_threads);
auto finity_counts = cuda::make_unique_span<count_t>(total_threads);
cuda::memory::zero(d_finity_counts);
auto events = std::make_pair(cuda::event::create(device), cuda::event::create(device));
auto timings = cuda::generate_unique_span<cuda::event::duration_t>(num_iterations,
[&](size_t) {
events.first.record();
cuda::launch(kernel<FCM>, device, launch_config, d_finity_counts.data());
events.second.record();
device.synchronize();
return cuda::event::time_elapsed_between(events.first, events.second);
});
std::nth_element(timings.begin(), timings.begin() + num_iterations / 2, timings.end());
std::cout << "isfinite check method " << std::left << std::setw(50) << name_of(FCM)
<< " : " << std::setw(12) << timings[num_iterations / 2].count() << " msec\n";
cuda::memory::copy(finity_counts, d_finity_counts);
sanity_check<FCM>(d_finity_counts, finity_counts);
}
int main()
{
auto device = cuda::device::current::get();
time_method<smaller_than_infinity_literal>(device);
time_method<using_cuda_intrinsic_for_float_type>(device);
time_method<less_than_ushort_threshold_value_for_non_finite>(device);
time_method<bit_pattern_comparison>(device);
time_method<bit_pattern_comparison_via_destructuring>(device);
time_method<self_subtraction>(device);
}