我有兴趣获得在GeForce GTX 550 Ti上执行1次双精度FLOP所需的纳秒数。
为了做到这一点,我遵循这种方法:我发现卡的单精度峰值性能是691.2 GFLOPS,这意味着双精度峰值性能将是它的1/8,即86.4 GFLOPS。然后,为了获得每个核心的FLOPS,我将86.4 GFLOPS除以核心数192,这样每个核心就可以得到0.45 GFLOPS。 0.45 GFLOPS意味着每个核心每纳秒0.25 FLOPS。如果我遵循正确的方法,那么我想知道每个核心运行多少线程来获取这些GFLOPS数字,我在哪里可以找到这些信息?
此外,我的小测试仅在一个线程中以236000232个周期执行。为了找到执行循环的1次迭代所花费的时间(以纳秒为单位),我做236000232/10 ^ 6 = 236次循环。卡的着色器时钟为1800Mhz,这意味着执行一次循环迭代需要236 / 1.8 = 131纳秒。这个数字远大于上面的数字(每个核心0.45纳秒)。我确信我在这里遗漏了一些东西,因为数字非常不同。请帮助我理解它背后的数学。
__global__ void bench_single(float *data)
{
int i;
double x = 1.;
clock_t start, end;
start = clock();
for(i=0; i<1000000; i++)
{
x = x * 2.388415813 + 1.253314137;
}
end = clock();
printf("End and start %d - %d\n", end, start);
printf("Finished in %d cycles\n", end-start);
}
谢谢,
答案 0 :(得分:4)
计算能力2.1设备具有每周期4次操作的双精度吞吐量(如果执行DFMA,则为8次)。假设所有32个线程在分派的warp中都处于活动状态。
4 ops / cycle / SM * 4 SMs * 1800 MHz * 2 ops / DFMA = 56 GFLOPS double
计算假定warp中的所有线程都处于活动状态。
您问题中的代码包含两个可以融合到DFMA中的相关操作。使用cuobjdump -sass来检查程序集。如果在同一SM上启动多个warp,则测试将变为依赖指令吞吐量而非延迟的度量。
答案 1 :(得分:3)
您应该意识到内核设计存在问题,这意味着使用此代码进行的任何测量都与双精度指令吞吐量完全无关。
因为包含所有双精度操作的计算循环的结果未在内存写入中使用,所以它将被编译器优化删除。 CUDA 5编译器为您的内核发出以下PTX:
.visible .entry _Z12bench_singlePf(
.param .u32 _Z12bench_singlePf_param_0
)
{
.local .align 8 .b8 __local_depot0[8];
.reg .b32 %SP;
.reg .b32 %SPL;
.reg .s32 %r<16>;
mov.u32 %SPL, __local_depot0;
cvta.local.u32 %SP, %SPL;
add.u32 %r3, %SP, 0;
.loc 2 13 1
cvta.to.local.u32 %r4, %r3;
// inline asm
mov.u32 %r1, %clock;
// inline asm
// inline asm
mov.u32 %r2, %clock;
// inline asm
st.local.v2.u32 [%r4], {%r2, %r1};
cvta.const.u32 %r5, $str;
// Callseq Start 0
{
.reg .b32 temp_param_reg;
.param .b32 param0;
st.param.b32 [param0+0], %r5;
.param .b32 param1;
st.param.b32 [param1+0], %r3;
.param .b32 retval0;
.loc 2 13 1
call.uni (retval0),
vprintf,
(
param0,
param1
);
ld.param.b32 %r6, [retval0+0];
}
// Callseq End 0
.loc 2 14 1
sub.s32 %r7, %r2, %r1;
cvta.const.u32 %r8, $str1;
st.local.u32 [%r4], %r7;
// Callseq Start 1
{
.reg .b32 temp_param_reg;
.param .b32 param0;
st.param.b32 [param0+0], %r8;
.param .b32 param1;
st.param.b32 [param1+0], %r3;
.param .b32 retval0;
.loc 2 14 1
call.uni (retval0),
vprintf,
(
param0,
param1
);
ld.param.b32 %r9, [retval0+0];
}
// Callseq End 1
.loc 2 15 2
ret;
}
两个时钟加载指令相邻,唯一的其他代码是printf
的调用。该PTX没有计算循环。
您应该重新设计内核,以便编译器无法推断出循环结果未被使用并将其优化掉。一种方法是这样的:
__global__
void bench_single(float *data, int flag=0)
{
int i;
double x = 1.;
clock_t start, end;
start = clock();
for(i=0; i<1000000; i++) {
x = x * 2.388415813 + 1.253314137;
}
end = clock();
printf("End and start %d - %d\n", end, start);
printf("Finished in %d cycles\n", end-start);
if (flag) {
data[blockIdx.x] = x;
}
}
内核末尾的条件写入会阻止编译器优化循环,所以现在编译器会发出这个PTX:
.visible .entry _Z12bench_singlePfi(
.param .u32 _Z12bench_singlePfi_param_0,
.param .u32 _Z12bench_singlePfi_param_1
)
{
.local .align 8 .b8 __local_depot0[8];
.reg .b32 %SP;
.reg .b32 %SPL;
.reg .pred %p<3>;
.reg .f32 %f<2>;
.reg .s32 %r<28>;
.reg .f64 %fd<44>;
mov.u32 %SPL, __local_depot0;
cvta.local.u32 %SP, %SPL;
ld.param.u32 %r6, [_Z12bench_singlePfi_param_0];
ld.param.u32 %r7, [_Z12bench_singlePfi_param_1];
add.u32 %r10, %SP, 0;
.loc 2 13 1
cvta.to.local.u32 %r1, %r10;
// inline asm
mov.u32 %r8, %clock;
// inline asm
mov.f64 %fd43, 0d3FF0000000000000;
mov.u32 %r27, 1000000;
BB0_1:
.loc 2 10 1
fma.rn.f64 %fd4, %fd43, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd5, %fd4, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd6, %fd5, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd7, %fd6, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd8, %fd7, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd9, %fd8, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd10, %fd9, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd11, %fd10, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd12, %fd11, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd13, %fd12, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd14, %fd13, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd15, %fd14, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd16, %fd15, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd17, %fd16, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd18, %fd17, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd19, %fd18, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd20, %fd19, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd21, %fd20, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd22, %fd21, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd23, %fd22, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd24, %fd23, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd25, %fd24, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd26, %fd25, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd27, %fd26, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd28, %fd27, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd29, %fd28, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd30, %fd29, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd31, %fd30, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd32, %fd31, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd33, %fd32, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd34, %fd33, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd35, %fd34, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd36, %fd35, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd37, %fd36, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd38, %fd37, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd39, %fd38, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd40, %fd39, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd41, %fd40, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd42, %fd41, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
fma.rn.f64 %fd43, %fd42, 0d40031B79BFF0AC8C, 0d3FF40D931FE078AF;
.loc 2 9 1
add.s32 %r27, %r27, -40;
setp.ne.s32 %p1, %r27, 0;
@%p1 bra BB0_1;
cvta.to.global.u32 %r5, %r6;
// inline asm
mov.u32 %r11, %clock;
// inline asm
.loc 2 13 1
st.local.v2.u32 [%r1], {%r11, %r8};
cvta.const.u32 %r12, $str;
// Callseq Start 0
{
.reg .b32 temp_param_reg;
.param .b32 param0;
st.param.b32 [param0+0], %r12;
.param .b32 param1;
st.param.b32 [param1+0], %r10;
.param .b32 retval0;
.loc 2 13 1
call.uni (retval0),
vprintf,
(
param0,
param1
);
ld.param.b32 %r14, [retval0+0];
}
// Callseq End 0
.loc 2 14 1
sub.s32 %r15, %r11, %r8;
cvta.const.u32 %r16, $str1;
st.local.u32 [%r1], %r15;
// Callseq Start 1
{
.reg .b32 temp_param_reg;
.param .b32 param0;
st.param.b32 [param0+0], %r16;
.param .b32 param1;
st.param.b32 [param1+0], %r10;
.param .b32 retval0;
.loc 2 14 1
call.uni (retval0),
vprintf,
(
param0,
param1
);
ld.param.b32 %r17, [retval0+0];
}
// Callseq End 1
.loc 2 16 1
setp.eq.s32 %p2, %r7, 0;
@%p2 bra BB0_4;
.loc 2 17 1
cvt.rn.f32.f64 %f1, %fd43;
mov.u32 %r18, %ctaid.x;
shl.b32 %r19, %r18, 2;
add.s32 %r20, %r5, %r19;
st.global.f32 [%r20], %f1;
BB0_4:
.loc 2 19 2
ret;
}
现在请注意,编译器部分展开循环的位置有一个很好的浮点乘加指令流。
正如格雷格史密斯所指出的那样,在给定SM上运行足够的warp以克服指令调度延迟之前,你不应该期望得到真正的指令吞吐量。这可能意味着您将要运行至少一个大块。另请注意,printf调用将对吞吐量产生很大的负面影响。如果每个块只有一个线程写出其结果,或者(更好地)将其存储到全局内存中,您将获得更具代表性的数字。运行大量的块,您将获得一些可以平均的测量值。作为最后的检查,您还应该使用cudaobjdump
反汇编目标代码,以确保汇编程序不会在时钟读取指令的位置移动,否则您依赖的内核时序将会中断。较旧版本的汇编程序有一个指令重新排序的习惯,它可能会破坏插入内核C代码或PTX的一系列时钟读取的功能。