我已经在c中实现了神经网络的整个训练,我尝试使用phtreads和CUDA将其转换为并行代码。串行反向传播的代码如下所示:
for (layer=LAYERS-2; layer>0; layer--)
{
for (neuron=0; neuron<neurons[layer]; neuron++)
{
temp_delta = 0.0;
for (next_neuron=0; next_neuron<neurons[layer+1]; next_neuron++)
{
temp_delta += WEIGHTS(layer+1,next_neuron,neuron)*DELTA(layer+1,next_neuron);
}
DELTA(layer,neuron) = temp_delta*((1.0)/(1.0+exp(-Z(layer,neuron))))*(1-((1.0)/(1.0+exp(-Z(layer,neuron))))); // δx,l=((wl+1)Tδx,l+1)*σ′(zx,l)
SUM_DELTA(thread_ID,layer,neuron) += DELTA(layer,neuron);
for (prev_neuron=0; prev_neuron<neurons[layer-1]; prev_neuron++)
{
AD(thread_ID,layer,neuron,prev_neuron) += DELTA(layer,neuron)*A(layer-1,prev_neuron);
}
}
}
和CUDA版本看起来像这样:
float *dev_weights;
float *dev_biases;
float *dev_sum_delta;
float *dev_ad;
float *dev_delta;
float *dev_z;
float *dev_a;
int *dev_neurons;
cudaMalloc(&dev_weights,LAYERS*max_neurons*max_neurons*sizeof(float));
cudaMalloc(&dev_biases,LAYERS*max_neurons*sizeof(float));
cudaMalloc(&dev_sum_delta,num_of_threads*LAYERS*max_neurons*sizeof(float));
cudaMalloc(&dev_ad,num_of_threads*LAYERS*max_neurons*max_neurons*sizeof(float));
cudaMalloc(&dev_delta,LAYERS*max_neurons*sizeof(float));
cudaMalloc(&dev_z,LAYERS*max_neurons*sizeof(float));
cudaMalloc(&dev_a,LAYERS*max_neurons*sizeof(float));
cudaMalloc(&dev_neurons,LAYERS*sizeof(int));
for (layer=LAYERS-2; layer>0; layer--)
{
cudaMemcpy(dev_weights,weights,LAYERS*max_neurons*max_neurons*sizeof(float),cudaMemcpyHostToDevice);
cudaMemcpy(dev_biases,biases,LAYERS*max_neurons*sizeof(float),cudaMemcpyHostToDevice);
cudaMemcpy(dev_delta,delta,LAYERS*max_neurons*sizeof(float),cudaMemcpyHostToDevice);
cudaMemcpy(dev_z,z,LAYERS*max_neurons*sizeof(float),cudaMemcpyHostToDevice);
cudaMemcpy(dev_a,a,LAYERS*max_neurons*sizeof(float),cudaMemcpyHostToDevice);
cudaMemcpy(dev_neurons,neurons,LAYERS*sizeof(float),cudaMemcpyHostToDevice);
backward<<<1,neurons[layer]>>>(dev_weights,dev_biases,dev_sum_delta,dev_ad,dev_delta,dev_z,dev_a,dev_neurons,layer,max_neurons,thread_ID);
cudaDeviceSynchronize();
cudaMemcpy(sum_delta,dev_sum_delta,num_of_threads*LAYERS*max_neurons*sizeof(float),cudaMemcpyDeviceToHost);
cudaMemcpy(ad,dev_ad,num_of_threads*LAYERS*max_neurons*max_neurons*sizeof(float),cudaMemcpyDeviceToHost);
cudaMemcpy(delta,dev_delta,LAYERS*max_neurons*sizeof(float),cudaMemcpyDeviceToHost);
}
backward()
CUDA内核:
__global__ void backward(float *weights, float *biases, float *sum_delta, float *ad, float *delta, float *z, float *a, int *neurons, int layer, int max_neurons, int thread_ID)
{
int neuron = threadIdx.x;
int next_neuron,prev_neuron;
float temp_delta=0.0;
for (next_neuron=0; next_neuron<neurons[layer+1]; next_neuron++)
{
temp_delta += WEIGHTS(layer+1,next_neuron,neuron)*DELTA(layer+1,next_neuron);
}
DELTA(layer,neuron) = temp_delta*((1.0)/(1.0+expf(-Z(layer,neuron))))*(1-((1.0)/(1.0+expf(-Z(layer,neuron))))); // δx,l=((wl+1)Tδx,l+1)*σ′(zx,l)
SUM_DELTA(thread_ID,layer,neuron) += DELTA(layer,neuron);
for (prev_neuron=0; prev_neuron<neurons[layer-1]; prev_neuron++)
{
AD(thread_ID,layer,neuron,prev_neuron) += DELTA(layer,neuron)*A(layer-1,prev_neuron);
}
}
无论我尝试过什么,这个并行代码都没有按预期工作,这意味着神经网络没有经过训练(成功率始终低于10%)。
串行代码100%正确(已测试)。
那么,有没有人知道从串口到CUDA的转换有什么问题?
答案 0 :(得分:-3)
机械转换与纯序列代码的工作方式不同。
为什么?
没有详细了解所有丢失的性能,预计会出现在[TIME]
- 复杂性域中(直接由层分离引入(由于[SPACE]
可能有一些可以想象的理由) - CUDA的复杂域限制可用的硬件资源))在CUDA内核中使用非原子操作意味着反向传播不能正确相加
(更新)
上面的实际内核代码使用backprop的仅拉模式,因此从前一层的神经元读取已经稳定(冷)的值,因此线程局部总和不受先前指示的风险的影响来自(非)-atomic ops
和
任何原子上不受保护的内核代码将随机依赖于CUDA线程的warp-concurrency冲突(巧合冲突出现并且必须出现在非受保护的操作中 - 只需尝试一个内核,在其中总结threadIdx.x
id - 在反向传播期间进行无保护的添加时,不使用原子的数字,你会看到这一点直接在一行随机输出数中实现这个主要是常数和)。
任何以绩效为导向的转型都需要适当的设计关注,以便:
不在计算策略中引入任何不连贯性(不产生废话)
不会增加更多的开销,而不是任何级别的并行度可能会调整(在开销方面失去的费用比在1/N
上获得希望加速的速度更多 - 分裂[PAR]
- 工作的一部分 - 避免不利的绩效影响...... for even more details on this issue, see this detailed explanation on overhead-strict Amdahl's Law on going parallel)
附加费用总额:
硬性事实比任何自以为是的言论都重要,所以让我们来看看。系统地测量,降至[ns]分辨率:sum_all_ADD_ON_SECTION_H2D .... [ns]
sum_all_ADD_ON_SECTION_D2H .... [ns]
sum_all_KERNEL_ASYNC_LOADS .... [ns]
sum_all_KERNEL_EXEC_TERMED .... [ns]
这些数字进入overhead-strict Amdahl's Law formula以查看真正的加速(希望保持在明显高于1.0的某个地方)。这不是被授予的,因为数学上是“弱”的。具有大量SMX非本地内存提取延迟的CUDA内核(aha,上面的拉模式)对于基于SMX的SIMD加速并不是那么完美 - 数学和#34;密集&#34;,SMX-local - 只有内核才能明显地获得当前硬件架构上更好的加速因素,但这两者都必须支付串行开销的成本 - 因此存在性能增益/损失的核心问题。
另请注意,here,罗伯特克罗维拉本人在上述实施方法中出现了额外的仅限串行调度性能的其他瓶颈。
这种拉模式CUDA内核相当简单&#34;浅#34;就上述原理而言,拉模式在技术上避免了第二级潜在的(不)可屏蔽延迟损害最小化......几乎可以忽略不计的效果,即使所有拉模式读取都可能表现出某种程度的合并读取。较新的硬件(除非使用标志进行编译)避免使用L1缓存,因此访问延迟会进一步增加(例如,图2.2中的Lx访问延迟从~300 [us]到~350-750的范围内)我们])
有关更多详细信息,请参阅绕过L1缓存( ref: here )
的进一步性能命中第二章背景 Kepler设备默认不使用L1数据高速缓存进行全局存储器访问,并且在出门到设备存储器DRAM之前首先使用L2高速缓存。可以使用
-dlcm=ca
nvcc编译器标志在某些Kepler系统中修改此行为。此默认行为与Fermi体系结构不同,Fermi体系结构默认使用L1和L2缓存,并具有编译器标志选项以禁用L1并仅使用共享L2缓存。启用后,L1高速缓存使用每个SMX 64KB内存的一部分,该内存分配给共享内存和L1使用。 L2高速缓存大小可以在硬件实现中变化。例如,K20和K40分别具有1280KB和1536KB的共享L2数据高速缓存。
for ( layer = LAYERS-2;
layer > 0;
layer-- ){
// ------------------------------clock.ADD_ON_SECTION_H2D.<START>
cudaMemcpy( dev_weights,
weights, LAYERS*sizeof(float)*max_neurons*max_neurons,
cudaMemcpyHostToDevice
);
cudaMemcpy( dev_biases,
biases, LAYERS*sizeof(float)*max_neurons,
cudaMemcpyHostToDevice
);
cudaMemcpy( dev_delta,
delta, LAYERS*sizeof(float)*max_neurons,
cudaMemcpyHostToDevice
);
cudaMemcpy( dev_z,
z, LAYERS*sizeof(float)*max_neurons,
cudaMemcpyHostToDevice
);
cudaMemcpy( dev_a,
a, LAYERS*sizeof(float)*max_neurons,
cudaMemcpyHostToDevice
);
cudaMemcpy( dev_neurons,
neurons, LAYERS*sizeof(float),
cudaMemcpyHostToDevice
);
// ------------------------------clock.ADD_ON_SECTION_H2D.<STOP>
// sum_ADD_ON_SECTION_H2D += clock.ADD_ON_SECTION_H2D
// ------------------------------clock.KERNEL_ASYNC_LOAD.<START>
backward<<<1,neurons[layer]>>>( dev_weights,
dev_biases,
dev_sum_delta,
dev_ad,
dev_delta,
dev_z,
dev_a,
dev_neurons,
layer,
max_neurons,
thread_ID
);
// ------------------------------clock.KERNEL_ASYNC_LOAD.<STOP>
// sum_all_KERNEL_ASYNC_LOADS += clock.KERNEL_ASYNC_LOAD
// ------------------------------clock.KERNEL_EXEC_TERMED.<START>
cudaDeviceSynchronize();
// ------------------------------clock.KERNEL_EXEC_TERMED.<STOP>
// sum_all_KERNEL_EXEC_TERMED += clock.KERNEL_EXEC_TERMED
// ------------------------------clock.ADD_ON_SECTION_D2H.<START>
cudaMemcpy( sum_delta,
dev_sum_delta, LAYERS*sizeof(float)*max_neurons*num_of_threads,
cudaMemcpyDeviceToHost
);
cudaMemcpy( ad,
dev_ad, LAYERS*sizeof(float)*num_of_threads*max_neurons*max_neurons,
cudaMemcpyDeviceToHost
);
cudaMemcpy( delta,
dev_delta, LAYERS*sizeof(float)*max_neurons,
cudaMemcpyDeviceToHost
);
// ------------------------------clock.ADD_ON_SECTION_D2H.<STOP>
// sum_all_ADD_ON_SECTION_D2H += clock.ADD_ON_SECTION_D2H
}
(非)-atomic ops:
__global__ void backward( float *weights,
float *biases,
float *sum_delta,
float *ad,
float *delta,
float *z,
float *a,
int *neurons,
int layer,
int max_neurons,
int thread_ID
)
{
int neuron = threadIdx.x;
int next_neuron,
prev_neuron;
float temp_delta = 0.0;
for ( next_neuron = 0;
next_neuron < neurons[layer+1];
next_neuron++
) {
temp_delta += WEIGHTS( layer+1, next_neuron, neuron )
* DELTA( layer+1, next_neuron );
}
DELTA( layer, neuron ) = temp_delta
* ( ( 1.0 )
/ ( 1.0
+ expf( -Z( layer, neuron ) )
)
)
* ( 1
- ( ( 1.0 )
/ ( 1.0
+ expf( -Z( layer, neuron )
)
)
)
); // δx,l=((wl+1)Tδx,l+1)*σ′(zx,l)
SUM_DELTA( thread_ID, layer, neuron ) += DELTA( layer, neuron );
for ( prev_neuron = 0;
prev_neuron < neurons[layer-1];
prev_neuron++
){
AD( thread_ID, layer, neuron, prev_neuron ) += DELTA( layer, neuron )
* A( layer-1, prev_neuron );
}
}