并行化伪代码以在GPU上工作:克服未对齐的内存访问

时间:2015-07-31 15:46:03

标签: cuda parallel-processing opencl gpgpu word2vec

这是我试图并行化的伪代码(取自word2vec C代码)。首先,我将列出具有相应大小的数据结构,然后是伪代码:

1.  long long sen[MAX_SENTENCE_LENGTH]  
// In the C code, MAX_SENTENCE_LENGTH = 1000. Increasing this should be  
//fine.

2.  float neu1[N] (hidden layer values)
//N is the length of each vector. For now, max N = 400

3.  float neu1e[N] (hidden layer error values)

4.  float syn0[V * N] (input to hidden layer weight matrix)
// For now, we can assume that V * N is small enough to be stored on the GPU
   // In the test data, V = 72k words

5.  float syn1neg[V * N] (back propagation weights used during negative  
sampling)

6. float exptable[1000] 

程序的输入是文本文件。程序然后一次处理一个单词以构建词汇表。例如,如果我的文本文件有一个句子

  

“并行编程非常有趣”

然后词汇表看起来像这样(因为代码根据单词的频率对词汇表进行排序):

            {“Very:2”, “Parallel:1”, “programming:1”, “is:1”,    “interesting:1”}
                   0      1               2              3                4

构建词汇表后,代码开始再次处理文本,一次1000个单词。前1000个单词存储在sen[MAX_SENTENCE_LENGTH]中,然后为sen中的所有单词训练神经网络,并且该过程一直持续到我们到达文件末尾。对于上述句子,sen将如下[1,2,3,0,0,4]

假设训练仅在一次迭代中完成,伪代码如下:

for sen in text
{ 
    for word in sen
    {

        for (c = 0; c < N; c++) 
            neu1[c] = 0;

        for (c = 0; c < N; c++) 
            neu1e[c] = 0;   

       /*The variable window is a user supplied parameter. 
        It is used to consider the context  around a word in a sentence. 
        For example, if I am looking at the first word in the sentence
        (target word is word1), and window = 5, then the words in the 
        window = {word2, word3, word4, word5}. 
        If I am looking at the third word in the sentence 
        (target word is word3), then window = {word1, word2, word4, word5}*/    

        for word in window
        {
            for (c = 0; c < N; c++) 
            neu1[c] += syn0[c + word * N];
        }

        for (c = 0; c < N; c++) 
            neu1[c] /= window;

        //negative: number of negative samples to provide (assume it to be 
             //between 5 to 25)
        for (d = 0; d < negative + 1; d++) 
        {

            target = sen[random_index]  
            l2 = target * N;
            f = 0;
            for (c = 0; c < N; c++) 
            f += neu1[c] * syn1neg[c + l2];

           gradient = exptable[function of f] //f is calculated in the loop above

           for (c = 0; c < N; c++) 
              neu1e[c] += gradient * syn1neg[c + l2];

           for (c = 0; c < N; c++) 
              syn1neg[c + l2] += gradient * neu1[c];

          } //Negative Sampling ends    

        for word in window
        {
             for (c = 0; c < N; c++) 
                syn0[c + word * N] += neu1e[c];
        }

   } // word in sen loop ends

 } // sen in text loop ends

我认为并行化的最佳方法是并行处理句子中的单词。考虑到所有循环,我认为每个单词应该使用N个线程,这样单个线程每个循环只访问一次全局内存(syn0, syn1neg)。此外,由于所有neu1neu1e更新都是独立的,因此它们可以驻留在线程的私有内存中并独立更新。

我现在主要关注的是:

  1. 全局内存访问以随机方式发生,因为syn0syn1neg是基于word变量的值(词汇表中的索引)访问的。而且,正如我们所看到的,句子中的单词不会以任何顺序出现。
  2. 这是一个大问题吗?或者,我们可以通过向GPU提供足够数量的线程来隐藏内存延迟吗?此外,我不明白这种访问模式是否实际上是随机的,因为N个线程/字将访问syn0和syn1neg中的顺序数据,但是下一组N个线程可以访问远离内存的顺序数据。

    1. 在负采样循环中,需要执行缩小操作。变量f是点积的总和。问题是我计划将neu1存储在每个线程的私有内存中,而syn1neg位于全局内存中。
    2. 负抽样是否需要单独的内核?看起来它需要一种不同于仅启动N个线程/单词的方法,但我不确定哪种方法最有效。

      除了这些问题之外,如果我接近此代码的方式存在问题,请提出建议。

1 个答案:

答案 0 :(得分:-2)

序言:您已经打开了一堆蠕虫(即使没有SLOC存在),所以希望您能接受每个部分提供的评论因为大象切片似乎是解决整个复杂主题的唯一方法,而不是从主要问题“逃避”到实现域的各个代码行的舒适区域,其中The Big图片通常会丢失,如果尚未丢失先验。

A1:,这是一个主要事实(又名“问题”)。

GPU - 设计设计&amp;在硅中优化为 SIMD - s ingle- i nstruction- m ultiple- d ata硬件架构,因此它们在 代码 +数据布局中表现最佳,不需要超过(在其整个生命周期内)确实很小的内存区域(千字节),适合SIMD SM内核的片上内存区域( GPU-SM-REGISTER -s,没有溢出效应,因此,LRU维护的L1缓存)因此没有引入任何“理想化” - 每about 350-700 ns次访问 gloMEM 的性能破坏性延迟惩罚。

  

[B]-Fig.2州:
TESLA SM 与8 [SMX] - 核心SM ,一对[SFU] SM ,一个多线程抓取&安培;问题[MTI] per TPC(纹理/处理器群集)
  shaMEM 一个16KB SM银行   每个conL1cache 一个只读SM (更快/更低的合并碰撞)
   [B] TECH.GPU:NVIDIA CUDA C编程指南,[PG-02829-001_v7.0]; 2015/03

这适用于栅格图像处理(以小块有组织的矩阵 - 卷积演算样式处理数据的2D数组),同时(当然, warpSize -segmented)time 所有线程在( 理想 )非冲突数据单元上执行相同的SIMD - 指令 ) - 这最适合GPGPU

这也意味着,任何现实生活中的操作都不会允许这种无意义的逐步渐进式完全对齐SIMD - 操作,并且自然会阻止性能(线程可以等待线程间的分歧) ,用于(重新) - 同步 - 障碍,用于远程存储器访问,直到数据传递最终发生并且因此延迟掩蔽越来越少隐藏这些自然障碍看到真实的错觉 PARALLEL 代码执行。

A2:不,几乎不多。原位基准测试方法和证据可用于定量评估其影响并证明所产生执行时间范围的限制。

虽然内核部署指令有一些帮助 __launch_bounds__()

__global__ void
__launch_bounds__( 1,     // dim3tbGridSIZE <maxThreadsPerBLOCK>         COMPILE-TIME_ADVICE_FOR_OPTIMISING_COMPILER_____________REGISTERs, CACHE_, FETCH_, PROXIMITY_PATTERNs ANALYSES
                   1  /*, // dim3tBlockSIZE <minBlocksPerMULTIPROCESSOR> COMPILE-TIME_ADVICE_FOR_OPTIMISING_COMPILER_____________OPTIMUM_SCHEDULE_TO_FILL_FETCH_LATENCIES
                   ?,     // iAsyncSeqOfCmdsQUEUE_Stream_ID <<- TO LET BE FREELY ASSIGNABLE ... NON-BLOCKING EXEC'd KERNEL
                   0  */  // iSharedMemSIZE
                   )
                 Device_printf_GPU_CLK( int const iTag ){
                        ...
                        return;
}

有很多论文发表在广泛的(强力扫描等)“优化”,而且机械调整,各种启动参数化的内核编译(线程3D-“几何”) ,对代码设计的一般假设的影响不应过高估计,因为结果始终是特定于内核的(并且在实践中,只是探讨在{{1}的有限资源部署期间哪个3D几何将受到最少的影响SMX内的硅 - 访问层次结构)。

虽然可以修改代码执行的线程3D-“几何”,但是延迟最关键的资源(GPU-off-chip-MEM s)是有限的,并且不能被其他线程“理想地重复使用”在调度期间进行上下文交换。您计划的线程越多,GPU-SM-REGISTER可以专门用于线程(各个计算兼容性XY的整体静态限制不是问题),而且越多 - 芯片内存访问将在真正的代码执行期间发生(为了得到这个事实,不需要编写GPU-SM-REGISTER,只需遵循已发布的架构文档)。

SLOC否。内核分离的想法可能会引入另一种线程3D几何排列的潜在好处的错觉,但是您的代码在支付额外费用时会遇到更多问题加载/共享协处理/发布数据结构的性能成本。内核分离在完全 A3: 设计的代码执行中是有意义的。