英特尔oneAPI——Cross Entropy并行实现与优化


前言

在进行Intel oneAPI异构计算学习之后,又有了交叉熵并行实现的需求,遂以此记录。


实现

这里主要是利用Intel的JupyterLab在CPU和GPU当中进行计算,通过CPU计算验证GPU并行计算的结果,并统计时间和效率。

交叉熵

交叉熵(cross entropy)在神经网络当中经常被用作损失值,要说清楚交叉熵的概念,还需要从熵说起。

熵是数学中用来量化不确定性的概念,即不确定性有多大,对应熵有一个多大的值。以掷骰子为例,一个六面的骰子如果每一面都是6点,那么掷骰子得到6的概率就是100%,而得到其他点数的概率为0,因此我们对结果十分确定,此时熵为0;
当骰子6面各不相同 ,掷骰子的结果均匀分布在1、2、3、4、5、6六点当中,此时熵最大,可以类比气体混合和固体溶解等自然界的熵增过程。

交叉熵的概念

熵是表达不确定性的概念,那么交叉熵是描述实际输出的可能性与预测可能性之间的匹配程度的指标。也就是预测结果和实际输出结果的概率分布越相近,其交叉熵越低。

反之两个概率差异越大,其交叉熵越大。

交叉熵计算

对于一个二维输入x[M][N] ,M代表分类数,N为特征数,计算其交叉熵作为损失值loss[N]。

Log Softmax:

公式1

Select:

公式2

Update:

公式3

整体计算过程如下图所示:

二维输入计算过程

任务目标

  • 输入一个三维矩阵x[K][M][N],K=128, M=32, N=8192, 其中K为batchsize,M为category,N为feature, 二维坐标数组mask[K][N], 权重数组weight[K][N]。
  • K维度相互独立,在每一个面做上述二维的cross entropy计算。
  • 最终结果为二维数组loss[K][N]。
  • 设计相应的GPU程序,要求结果和CPU计算结果误差绝对值在0.001之内。
  • 用时间来衡量程序的性能,越小越好,给出相关的实验数据和分析。

实现

根据上面的计算过程,以下代码包括两个主要部分,一个是CPU交叉熵计算,另一部分是GPU交叉熵计算,
详细步骤见下方代码以及注释部分。

%%writefile lab/cross_entropy.cpp

#include<iostream>
#include<chrono>
#include<cmath>
#include<CL/sycl.hpp>

#define random_float()  (rand() / double(RAND_MAX));

using namespace std;
using namespace sycl;


// Input size
constexpr int K = 128;
constexpr int M = 2048;
constexpr int N = 128;

constexpr int iterations = 10;

//GPU计算
float gpu_kernel(float* X, int* mask, float* weight, float* loss, queue& q) {

    float duration = 0.0;
    auto e = q.submit([&](handler& h) {

        //对于该计算,K维度和N维度相互独立,这里也用K*N作为并行化的range
        h.parallel_for( K*N, [=](auto& idx) {

            //K为行,下面是行列值的计算
            int row = idx / N ;
            int col = idx % N ;

            float exp_sum = 0.0;
            for (int i = 0; i < M; ++i) {
                exp_sum += exp(X[row * M * N + i * N + col]);	//计算每一个M列的exp值之和
            }

            int mask_id = mask[row * N + col];	//选择K,N二维列表对应掩膜值
            loss[row * N + col] = weight[row * N + col] * log(exp(X[row * M * N + mask_id * N + col]) / exp_sum);//找出对应的xi值,计算yi值,并乘以对应的权重值得到loss值

            });
        });

    e.wait();//阻塞等等该计算完成

    // 获取GPU kernal的开始和介绍时间戳之差(ns),转换成ms
    duration = (e.get_profiling_info<info::event_profiling::command_end>() - e.get_profiling_info<info::event_profiling::command_start>()) / 1000.0f / 1000.0f;

    return duration;
}

//CPU计算
float cpu_kernel(float* X, int* mask, float* weight, float* loss) {
    double duration = 0.0;
    chrono::high_resolution_clock::time_point s, e;

    s = chrono::high_resolution_clock::now();//获取运算开始时间戳
    for (int i = 0; i < K; ++i) {
        for (int j = 0; j < N; ++j) {
            float exp_sum = 0.0;

            for (int k = 0; k < M; ++k) {
                exp_sum += exp(X[i * M * N + k * N + j]);//计算每一个M列的exp值之和
            }
			//计算过程与GPU相同
            int mask_id = mask[i * N + j];

            loss[i * N + j] = weight[i * N + j] * (log(exp(X[i * M * N + mask_id * N + j]) / exp_sum));
        }
    }
    e = chrono::high_resolution_clock::now();
    duration = chrono::duration<float, milli>(e - s).count();//计算CPU时间差(ms)

    return duration;
}

//对比CPU和GPU计算结果,值相差超过0.001认为计算错误
int verify(float* data_host, float* data_device) {
    int errCount = 0;
    for (int i = 0; i < K * N; ++i) {
        if (fabs(data_host[i] - data_device[i]) > 0.001) ++errCount;
    }
    return errCount;//返回统计的错误的值数量
}

void run(queue& q) {

    //在主机当中申请存储数据的变量
    float* X_cpu = malloc_host<float>(K * M * N, q);//输入变量,大小为K*M*N
    int*   mask_cpu = malloc_host<int>(K * N, q);	//计算维度在K*N是独立的,因此这两个变量是K*N
    大小
    float* weight_cpu = malloc_host<float>(K * N, q);

    //申请GPU当中变量
    float* X_gpu = malloc_device <float>(K * M * N, q);
    int*   mask_gpu = malloc_device <int>(K * N, q);
    float* weight_gpu = malloc_device <float>(K * N, q);

    //用UMS存储GPU计算结果,便于获取
    float* loss_gpu = malloc_shared <float>(K * N, q);

    //CPU计算结果
    float* loss_cpu = malloc_host<float>(K * N, q);

    //随机生成输入值
    for (int i = 0; i < K * M * N; ++i) {
        X_cpu[i] = random_float();
    }
	//初始化中间变量
    for (int i = 0; i < K * N; ++i) {
        loss_gpu[i] = 0.0;
        loss_cpu[i] = 0.0;
        mask_cpu[i] = i % M;
        weight_cpu[i] = random_float();
    }

    //将变量值拷贝到GPU当中
    q.memcpy(X_gpu, X_cpu, sizeof(float) *K * M * N).wait();
    q.memcpy(mask_gpu, mask_cpu, sizeof(int) * K * N).wait();
    q.memcpy(weight_gpu, weight_cpu, sizeof(float)* K * N).wait();
    
    float duration_cpu = 0.0;
    float duration_gpu = 0.0;

	//统计多次计算耗时,求平均值
    int warmup = 10;
    for (int i = 0; i < iterations / 2 + warmup / 2; ++i) {
        float duration = cpu_kernel(X_cpu, mask_cpu, weight_cpu, loss_cpu);
        if (i >= warmup / 2) duration_cpu += duration;
    }
    duration_cpu /= iterations / 2;

    for (int i = 0; i < iterations + warmup; ++i) {
        float duration = gpu_kernel(X_gpu, mask_gpu, weight_gpu, loss_gpu, q);
        if (i >= warmup) duration_gpu += duration;
    }
    duration_gpu /= iterations;

    printf("Cross Entropy Input Size: K: %d, M: %d, N: %d, Total : %d\n"
        "GPU time: %lf (ms)\n"
        "CPU time: %lf (ms)\n",
        K, M, N, K * M * N, duration_gpu, duration_cpu);

    int errCount = 0;
    errCount = verify(loss_cpu, loss_gpu);
    printf("%d errors in loss_gpu\n", errCount);//检验结果


    // free all memory in host 
    free(loss_cpu, q);
    free(X_cpu, q);
    free(mask_cpu, q);
    free(weight_cpu, q);

    // free all memory in device 
    free(loss_gpu, q);
    free(X_gpu, q);
    free(mask_gpu, q);
    free(weight_gpu, q);
}

int main() {
    //指定enable_profiling()属性
    auto propList = property_list{ property::queue::enable_profiling() };

    //GPU选择器
    queue my_gpu_queue(gpu_selector{}, propList);

    run(my_gpu_queue);

    return 0;
}

运行结果

CPU运算平均耗时约47ms,GPU耗时约7ms,加速比约为7。

运行结果

改进优化

在上面的步骤当中,我们可以看出,输入的维度是K*M*N,无相关性的维度是K*N,因此在K*N维度上可以直接进行并行化,这在K和N较大时其加速比较大,但是如果M较大的话,GPU并行程度低,其加速比就会降低。

在对M维计算时,其exp值被重复计算了两次,并且求和过程没有进行并行化,因此这些地方都可以进行优化,后面再继续更新。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值