【CUDA】 矩阵乘向量 matVecMul

news/2024/7/8 6:20:47 标签: c语言, c++, 深度学习, cuda

Matrix - Vector Multiplication

矩阵-向量乘法是线性代数中的基本操作。它用于将一个矩阵与一个向量相乘。乘法的结果是与输入向量大小相同的向量。

矩阵和向量的乘法如图1所示。

图1

基础kernel与共享内存kernel

执行矩阵-向量乘法的基础kernel是使用单个线程执行输出向量的单个元素的乘法。这意味着所需的线程数等于输出向量中的元素数。线程以一维网格排列,每个线程被分配一个唯一的索引。线程的索引用于访问输入矩阵的对应行。对行和向量进行乘法操作,结果存储在输出向量的对应元素中。

在这种基础kernel中,每个线程将加载整个输入向量。这并不高效,因为输入向量被多次加载。为了避免这种情况,我们可以使用一个tile将输入向量存储在共享内存中。tile是一个大小等于块中线程数的一维数组。向量被加载到tile中,每个线程使用tile执行乘法。

Code

Host代码初始化输入矩阵和向量,随机赋值并调用kernel执行乘法。输入矩阵以线性化格式存储。

#include <iostream>
#include <cstdio>
#include <ctime>
#include <cmath>

#include <cuda_runtime.h>
#include <thrust/host_vector.h>
#include <thrust/device_vector.h>
#include "image2gray.cuh"
#include "helper_cuda.h"
#include "error.cuh"

const int FORTIME = 50;

int main(int argc, char* argv[])
{
    int rows, cols, t_x;

    rows = 4096;
    cols = 4096;
    t_x = 1024;

    thrust::host_vector<float> h_in_mat(rows * cols);
    thrust::host_vector<float> h_in_vec(cols);

    srand(time(NULL));
    for (int i = 0; i < rows * cols; i++) {

        h_in_mat[i] = rand() / (float)RAND_MAX;

        if (i < cols)
            h_in_vec[i] = rand() / (float)RAND_MAX;
    }


    thrust::host_vector<float> h_out(rows);

    for (int i = 0; i < rows; ++i)
        for (int j = 0; j < cols; ++j)
            h_out[i] += h_in_mat[i * cols + j] * h_in_vec[j];

    thrust::device_vector<float> d_in_mat = h_in_mat;
    thrust::device_vector<float> d_in_vec = h_in_vec;

    thrust::device_vector<float> d_out(rows);

    dim3 block(t_x);
    dim3 grid((rows + block.x - 1) / block.x);

    cudaEvent_t start, stop;
    checkCudaErrors(cudaEventCreate(&start));
    checkCudaErrors(cudaEventCreate(&stop));
    checkCudaErrors(cudaEventRecord(start));
    for (int i = 0; i < FORTIME; i++)
        mat_vec_mul << <grid, block >> > (
            thrust::raw_pointer_cast(d_out.data()),
            thrust::raw_pointer_cast(d_in_mat.data()),
            thrust::raw_pointer_cast(d_in_vec.data()),
            rows, cols);
    checkCudaErrors(cudaEventRecord(stop));
    checkCudaErrors(cudaEventSynchronize(stop));
    float elapsed_time;
    checkCudaErrors(cudaEventElapsedTime(&elapsed_time, start, stop));
    std::cout << "elapsed_time:" << elapsed_time / FORTIME << std::endl;

    bool success = true;
    for (int i = 0; i < rows; ++i)
        if (abs(h_out[i] - d_out[i]) >= 0.001) {
            std::cout << "Error at " << i << ": " << h_out[i] << " != " << d_out[i] << " (Mat Vec Mul)" << std::endl;
            success = false;
            break;
        }
    if (success)
        std::cout << "Success (Mat Vec Mul)" << std::endl;


    checkCudaErrors(cudaEventRecord(start));
    for (int i = 0; i < FORTIME; i++)
    mat_vec_mul_tiles << <grid, block, block.x * sizeof(float) >> > (
        thrust::raw_pointer_cast(d_out.data()),
        thrust::raw_pointer_cast(d_in_mat.data()),
        thrust::raw_pointer_cast(d_in_vec.data()),
        rows, cols);
    checkCudaErrors(cudaEventRecord(stop));
    checkCudaErrors(cudaEventSynchronize(stop));
    checkCudaErrors(cudaEventElapsedTime(&elapsed_time, start, stop));
    std::cout << "elapsed_time:" << elapsed_time / FORTIME << std::endl;

    success = true;
    for (int i = 0; i < rows; ++i)
        if (abs(h_out[i] - d_out[i]) >= 0.001) {
            std::cout << "Error at " << i << ": " << h_out[i] << " != " << d_out[i] << " (Mat Vec Mul Tiles)" << std::endl;
            success = false;
            break;
        }
    if (success)
        std::cout << "Success (Mat Vec Mul Tiles)" << std::endl;

    return 0;
}

Note:

helper_cuda.h 与error.cuh头文件为错误查验工具。后续会发布到Github。

去除checkCudaErrors等错误查验函数不影响程序运行。


下面显示了两个kernel。

template<typename T> __global__
void mat_vec_mul(T *out, T *in_mat, T *in_vec, int m, int n) {

    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if(i >= m) return;
    
    T temp = 0;
    for (int k = 0; k < n; ++k)
        temp += in_mat[i * n + k] * in_vec[k];
    out[i] = temp;
}

template<typename T> __global__
void mat_vec_mul_tiles(T *out, T *in_mat, T *in_vec, int m, int n) {

    extern __shared__ uint8_t tiles_uint8[];
    T *tiles = reinterpret_cast<T*>(tiles_uint8);

    int i = blockIdx.x * blockDim.x + threadIdx.x;

    T res = 0;
    int n_phases = (n + blockDim.x - 1) / blockDim.x;
    for (int phase = 0; phase < n_phases; ++phase){

        int elem_idx = phase * blockDim.x + threadIdx.x;
        tiles[threadIdx.x] = elem_idx < n ? in_vec[elem_idx] : 0;
        __syncthreads();

        if(i < m)
            for (int k = 0; k < blockDim.x && phase * blockDim.x + k < n; ++k)
                res += in_mat[i * n + phase * blockDim.x + k] * tiles[k];
        __syncthreads();
    }
    out[i] = res;
}

基础kernel

kernel首先计算线程的索引。如果索引在输出向量的范围内,则执行乘法。

int i = blockIdx.x * blockDim.x + threadIdx.x;
if(i >= m) return;

将矩阵的每一列与向量相应元素相乘,并将结果存储在输出向量的相应元素中。

T temp = 0;
for (int k = 0; k < n; ++k)
    temp += in_mat[i * n + k] * in_vec[k];
out[i] = temp;

共享内存kernel

kernel首先计算线程的索引。kernel不会检查边界条件,因为需要加载向量到tile中的超出输出向量范围的线程。

int i = blockIdx.x * blockDim.x + threadIdx.x;

现在,乘法被分成几个阶段。在每个阶段,线程将把向量的单个或零个元素(如果它们超出范围)加载到tile中。然后线程将执行加载到tile中的向量和输入矩阵行的相应元素的乘法。结果存储在输出向量的相应元素中。

在加载向量到tile中和执行乘法时,线程都会执行边界检查,以避免超出范围的访问。

T res = 0;
int n_phases = (n + blockDim.x - 1) / blockDim.x;
for (int phase = 0; phase < n_phases; ++phase){

    int elem_idx = phase * blockDim.x + threadIdx.x;
    tiles[threadIdx.x] = elem_idx < n ? in_vec[elem_idx] : 0;
    __syncthreads();

    if(i < m)
        for (int k = 0; k < blockDim.x && phase * blockDim.x + k < n; ++k)
            res += in_mat[i * n + phase * blockDim.x + k] * tiles[k];
    __syncthreads();
}
out[i] = res;

性能分析

运行时间:

矩阵维度:4096 × 4096

向量维度:4096

线程块维度:1024

由运行时间分析,引入共享内存效果比较明显,而且经测试输入向量维度越大,共享内存效果越明显。

Note:单次运行可能因为设备启动原因,各种算法运行时间差异较大,可采用循环20次以上取平均值。

笔者采用设备:RTX3060 6GB

PMPP项目提供的分析

kernel的性能是使用NvBench项目在多个gpu中测量的。研究的性能测量方法有:

内存带宽:每秒传输的数据量。

内存带宽利用率:占用内存带宽的百分比。

基础kernel

共享内存kernel

参考文献:

1、大规模并行处理器编程实战(第2版)

2、​​​PPMP


http://www.niftyadmin.cn/n/5536672.html

相关文章

网站UI:我只负责漂亮,实现的事情交给前端开发。

网页UI的美观性对于用户体验和网站的成功至关重要&#xff0c;以下是几个原因&#xff1a; 吸引用户&#xff1a;美观的网页UI可以吸引用户的注意力&#xff0c;使用户对网站产生兴趣并留下深刻印象。用户更有可能在美观的界面上停留更长时间&#xff0c;探索网站的功能和内容…

宝塔面板Nginx的https301跳转http

宝塔面板Nginx的https301跳转http&#xff1a; 登录宝塔面板&#xff1a;进入你的宝塔面板管理界面。 选择网站管理&#xff1a;在左侧菜单中&#xff0c;点击“网站”&#xff0c;然后选择你需要进行重定向的网站。 配置网站设置&#xff1a;在所选网站的管理界面中&#x…

linux下删除当前路径下的所有文件夹但保留文件

打开终端&#xff0c;输入&#xff0c; find . -mindepth 1 -maxdepth 1 -type d -exec rm -r {} 解释&#xff1a; find是查找文件和文件夹的命令。.表示当前路径。-mindepth 1表示最小搜索深度为1&#xff0c;这样不会包括当前目录。-maxdepth 1表示最大搜索深度为1&#x…

VScode在linux下调试代码备忘

0、资料来源 https://www.bilibili.com/video/BV13K411M78v? p2&spm_id_frompageDriver 该视频讲解了vscode在window下配置单个源文件/多个源文件/CMakeLists.txt工程&#xff0c;如何进行调试 在此基础上测试了linux下vscode的调试&#xff0c;并通过测试&#xff0c;…

典型案例 | 基于全数字实时仿真的嵌入式DevOps解决方案

为丰富浙江省信息技术应用创新&#xff08;以下简称“信创”&#xff09;产业生态&#xff0c;在全社会各领域形成示范效应&#xff0c;浙江省经信厅联合省密码管理局开展2023年浙江省深化信创典型案例评选工作。 经过征集申报、专家评选、名单公示等程序&#xff0c;确定36个…

VSCode使用Makefile管理工程

Visual Studio Code&#xff08;VSCode&#xff09; 是一个高度可定制的代码编辑器&#xff0c;支持广泛的编程语言和构建系统。通过使用 Makefile 和适当的扩展&#xff0c;可以轻松地使用 VSCode 来管理基于 Make 的项目。以下是详细步骤&#xff0c;帮助你在 VSCode 中使用 …

软件测试面试200问【答案+文档】

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、B/S架构和C/S架构区别 B/S 只需要有操作系统和浏览器就行&#xff0c;可以实现跨平台&#x…

微信小程序 typescript 开发日历界面

1.界面代码 <view class"o-calendar"><view class"o-calendar-container" ><view class"o-calendar-titlebar"><view class"o-left_arrow" bind:tap"prevMonth">《</view>{{year}}年{{month…