cuBLAS矩阵乘法性能分析(附代码示例)

本文涉及的产品
函数计算FC,每月15万CU 3个月
简介: 矩阵乘法是神经网络中最基础、最重要的一个运算。在用CUDA实现矩阵乘法时,不需要我们手动写,cuBLAS库提供了现成的矩阵乘法算子,例如cublasGemmEx和cublasLtMatmul。其中后者是轻量级版本,API调用更灵活。例如对于整数乘法,cublasLtMatmul支持int8的输入输出,而cublasGemmEx只支持int8输入,int32输出

使用教程


矩阵乘法是神经网络中最基础、最重要的一个运算。在用CUDA实现矩阵乘法时,不需要我们手动写,cuBLAS库提供了现成的矩阵乘法算子,例如cublasGemmExcublasLtMatmul。其中后者是轻量级版本,API调用更灵活。例如对于整数乘法,cublasLtMatmul支持int8的输入输出,而cublasGemmEx只支持int8输入,int32输出。

今天我只给大家讲解cublasGemmEx,主要使用起来相对更简洁一点。

官方文档地址:https://docs.nvidia.com/cuda/cublas/index.html#cublas-GemmEx

经过翻阅网上各种教程,我找到了一篇我认为写的最好的博客。例子举得非常好,写的很详细。地址如下:https://www.cnblogs.com/cuancuancuanhao/p/7763256.html

具体的使用方法可以参见上面这篇博客,我这里就不再赘述了。

今天我主要给大家演示一下,不同数据类型的矩阵乘法,速度和结果上到底有多大的差异?

测试代码


我写了一个简单的测试代码:

#include <sys/time.h>
#include <cuda_profiler_api.h>
#include <cublas_v2.h>
#include <cuda.h>
#include <cuda_fp16.h>
#include <cuda_runtime.h>
#include <stdio.h>
int8_t float2int8(float f, float scale) {
    int8_t i = int8_t(f * scale);
    if (i < -127) i = -127;
    if (i > 127) i = 127;
    return i;
}
template <typename T, typename S>
void allocate_memory(int m, int n, int k, T **A, T **B, S **C) {
    cudaMallocManaged(A, m * k * sizeof(T));
    cudaMallocManaged(B, k * n * sizeof(T));
    cudaMallocManaged(C, m * n * sizeof(S));
}
template <typename T, typename S>
void free_memory(T *A, T *B, S *C) {
    cudaFree(A);
    cudaFree(B);
    cudaFree(C);
}
template <typename T, typename S>
int cublas_gemm_ex(cublasHandle_t handle, cublasOperation_t transA, cublasOperation_t transB,
                   int m, int n, int k, T *A, T *B, S *C, int lda, int ldb, int ldc,
                   S *alpha, S *beta, int algo) {
    cudaDataType_t AType, BType, CType, ComputeType;
    if (std::is_same<T, float>::value) {
        AType = BType = CType = ComputeType = CUDA_R_32F;
    } else if (std::is_same<T, __half>::value) {
        AType = BType = CType = ComputeType = CUDA_R_16F;
    } else if (std::is_same<T, int8_t>::value) {
        AType = BType = CUDA_R_8I;
        CType = ComputeType = CUDA_R_32I;
    } else {
        printf("Not supported data type.");
        return -1;
    }
    cublasStatus_t status;
    status = cublasGemmEx(handle,
                          transA,
                          transB,
                          m,
                          n,
                          k,
                          alpha,
                          A,
                          AType,
                          lda,
                          B,
                          BType,
                          ldb,
                          beta,
                          C,
                          CType,
                          ldc,
                          ComputeType,
                          static_cast<cublasGemmAlgo_t>(algo));
    if (status == CUBLAS_STATUS_SUCCESS)
        return 1;
    else
        return -1;
}
template <typename T, typename S>
void test_gemm(cublasHandle_t handle, int m, int n, int k, T *A, T *B, S *C,
               S *alpha, S *beta, int algo, int iteration) {
    float total_time = 0;
    for (int i = 0; i < iteration; ++i) {
        struct timeval start, end;
        cudaDeviceSynchronize();
        cudaProfilerStart();
        gettimeofday(&start, NULL);
        int success = cublas_gemm_ex(handle,
                                     CUBLAS_OP_N,
                                     CUBLAS_OP_N,
                                     n,
                                     m,
                                     k,
                                     B,
                                     A,
                                     C,
                                     n,
                                     k,
                                     n,
                                     alpha,
                                     beta,
                                     static_cast<cublasGemmAlgo_t>(algo));
        cudaDeviceSynchronize();
        gettimeofday(&end, NULL);
        cudaProfilerStop();
        if (success > 0 && i > 0)
            total_time += (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) * 0.001;
    }
    if (total_time > 0)
        printf("algo %d: %.3f ms\n", algo, total_time / (iteration - 1));
}
int main() {
    int m = 4096, n = 8192, k = 1024;
    printf("shape: (%d, %d) x (%d, %d)\n", m, k, k, n);
    int start_algo = CUBLAS_GEMM_DEFAULT;
    int end_algo = CUBLAS_GEMM_ALGO23;
    int start_algo_t_op = CUBLAS_GEMM_DEFAULT_TENSOR_OP;
    int end_algo_t_op = CUBLAS_GEMM_ALGO15_TENSOR_OP;
    int iteration = 10;
    float *fA, *fB, *fC;
    __half *hA, *hB, *hC;
    int8_t *iA, *iB; int32_t *iC;
    float f_alpha = 1, f_beta = 0;
    __half h_alpha = __float2half_rn(1.0), h_beta = __float2half_rn(0.0);
    int32_t i_alpha = 1, i_beta = 0;
    allocate_memory(m, n, k, &fA, &fB, &fC);
    allocate_memory(m, n, k, &hA, &hB, &hC);
    allocate_memory(m, n, k, &iA, &iB, &iC);
    for (int i = 0; i < m * k; ++i) {
        fA[i] = float(i % 255 - 127) / 127;
        hA[i] = __float2half_rn(fA[i]);
        iA[i] = float2int8(fA[i], 127);
    } 
    for (int i = 0; i < k * n; ++i) {
        fB[i] = float(i % 255 - 127) / 127;
        hB[i] = __float2half_rn(fB[i]);
        iB[i] = float2int8(fB[i], 127);
    }
    cublasHandle_t handle;
    cublasCreate(&handle);
    printf(">>>>>>>>>>>>>>>>> test fp32 >>>>>>>>>>>>>>>>>\n");
    for (int algo = start_algo; algo <= end_algo; ++algo)
        test_gemm(handle, m, n, k, fA, fB, fC, &f_alpha, &f_beta, algo, iteration);
    for (int algo = start_algo_t_op; algo <= end_algo_t_op; ++algo)
        test_gemm(handle, m, n, k, fA, fB, fC, &f_alpha, &f_beta, algo, iteration);
    printf(">>>>>>>>>>>>>>>>> test fp16 >>>>>>>>>>>>>>>>>\n");
    for (int algo = start_algo; algo <= end_algo; ++algo)
        test_gemm(handle, m, n, k, hA, hB, hC, &h_alpha, &h_beta, algo, iteration);
    for (int algo = start_algo_t_op; algo <= end_algo_t_op; ++algo)
        test_gemm(handle, m, n, k, hA, hB, hC, &h_alpha, &h_beta, algo, iteration);
    printf(">>>>>>>>>>>>>>>>> test int8 >>>>>>>>>>>>>>>>>\n");
    for (int algo = start_algo; algo <= end_algo; ++algo)
        test_gemm(handle, m, n, k, iA, iB, iC, &i_alpha, &i_beta, algo, iteration);
    for (int algo = start_algo_t_op; algo <= end_algo_t_op; ++algo)
        test_gemm(handle, m, n, k, iA, iB, iC, &i_alpha, &i_beta, algo, iteration);
    printf(">>>>>>>>>>>>>>>>> compare result >>>>>>>>>>>>>>>>>\n");
    printf("fp32: ");
    for (int i = 0; i < 10; ++i)
        printf("%.5f%c", fC[i], " \n"[i==9]);
    printf("fp16: ");
    for (int i = 0; i < 10; ++i)
        printf("%.5f%c", float(hC[i]), " \n"[i==9]);
    printf("int8: ");
    for (int i = 0; i < 10; ++i)
        printf("%.5f%c", float(iC[i])/127/127, " \n"[i==9]);
    free_memory(iA, iB, iC);
    free_memory(fA, fB, fC);
    free_memory(hA, hB, hC);
    return 0;
}

代码保存为test_gemm.cpp,然后执行下面命令进行编译:

nvcc test_gemm.cpp -o test_gemm -L/usr/local/cuda/lib64 -lcudart -lcuda -lcublas

最后执行./test_gemm运行就行了。


image.png

运行结果


我对比了三种数据类型:fp32fp16int8,测试环境是V100显卡、CUDA 10.1。由于V100显卡没有int8的tensor core,所以速度并不能达到最快。要想全速进行int8的矩阵乘法,推荐使用sm75及以上的显卡,例如T4、A100等等。此外我还对比了不同的GEMM算法的效果。

执行上面的运行命令后,会输出如下的结果:

shape: (4096, 1024) x (1024, 8192)
>>>>>>>>>>>>>>>>> test fp32 >>>>>>>>>>>>>>>>>
algo -1: 4.831 ms
algo 2: 5.293 ms
algo 3: 5.406 ms
algo 4: 5.297 ms
algo 5: 5.098 ms
algo 6: 4.874 ms
algo 11: 4.870 ms
algo 18: 7.219 ms
algo 19: 6.061 ms
algo 20: 5.631 ms
algo 99: 1.110 ms
algo 100: 1.159 ms
algo 101: 1.688 ms
algo 102: 4.944 ms
algo 103: 4.744 ms
algo 104: 4.700 ms
algo 105: 4.679 ms
algo 106: 4.679 ms
algo 107: 4.675 ms
algo 108: 4.676 ms
algo 109: 4.677 ms
algo 110: 4.676 ms
algo 111: 4.676 ms
algo 112: 4.678 ms
algo 113: 4.675 ms
algo 114: 4.676 ms
algo 115: 4.689 ms
>>>>>>>>>>>>>>>>> test fp16 >>>>>>>>>>>>>>>>>
algo -1: 2.423 ms
algo 1: 2.460 ms
algo 2: 2.565 ms
algo 3: 2.518 ms
algo 5: 2.398 ms
algo 6: 2.416 ms
algo 99: 0.737 ms
algo 100: 1.581 ms
algo 101: 1.032 ms
algo 102: 0.978 ms
algo 103: 0.767 ms
algo 104: 0.790 ms
algo 105: 0.803 ms
algo 106: 0.774 ms
algo 107: 2.656 ms
algo 108: 2.577 ms
algo 109: 2.518 ms
algo 110: 0.925 ms
algo 111: 0.951 ms
algo 112: 0.935 ms
algo 113: 0.909 ms
algo 114: 2.549 ms
algo 115: 2.532 ms
>>>>>>>>>>>>>>>>> test int8 >>>>>>>>>>>>>>>>>
algo -1: 1.232 ms
algo 0: 7.544 ms
algo 1: 1.217 ms
algo 2: 1.294 ms
algo 3: 2.362 ms
algo 99: 1.243 ms
algo 100: 1.244 ms
algo 101: 1.237 ms
algo 102: 1.232 ms
algo 103: 1.230 ms
algo 104: 1.224 ms
algo 105: 1.222 ms
algo 106: 1.224 ms
algo 107: 1.225 ms
algo 108: 1.224 ms
algo 109: 1.218 ms
algo 110: 1.217 ms
algo 111: 1.217 ms
algo 112: 1.218 ms
algo 113: 1.218 ms
algo 114: 1.216 ms
algo 115: 1.217 ms
>>>>>>>>>>>>>>>>> compare result >>>>>>>>>>>>>>>>>
fp32: 52.38629 44.76633 37.65229 31.04420 24.94203 19.34578 14.25543 9.67102 5.59253 2.01996
fp16: 52.46875 44.84375 37.40625 31.21875 24.95312 19.39062 14.28125 9.69531 5.61328 2.05078
int8: 52.38626 44.76632 37.65230 31.04421 24.94203 19.34577 14.25544 9.67103 5.59254 2.01996

这里简单解释一下,algo -1到23表示不使用tensor core算法的结果,algo 99到115表示使用tensor core算法的结果。

可以看到图中缺失了一部分算法的结果,因为那些算法可能不适用于当前的矩阵乘法,因此报错了。

汇总一下各自最快的结果(不使用vs使用tensor core):

  • fp32: 4.83 1.11
  • fp16: 2.41 0.73
  • int8: 1.21 1.21

由于V100显卡没有int8的tensor core,所以int8的两个结果是相同的。结果也符合我们的预期,速度上fp32慢于fp16慢于int8。所以在实际的深度学习应用中,流行使用混合精度,也就是用fp16来进行训练和推理。

而int8是速度最快的,所以如果训练和推理也都能使用int8的话,速度上将会迈上一个新的台阶。

那么一个浮点数的矩阵乘法怎么转变为整数的矩阵乘法呢?这里我不会详细讲,后续会出一个详细的量化教程。


image.png

那么由于这里有个类型转换的操作,所以会产生误差。但是在我们的样例中,int8的误差竟然比fp16还要小很多,结果和fp32几乎一模一样。这主要由于是我构造的矩阵数据分布非常均匀有规律,因此计算误差会很小,实际深度网络中int8的误差会较大。

结语


int8甚至更低比特的量化的实际收益非常大,提速可以达到将近2倍。虽然现在有很多现成的自动量化工具,但是效果上或多或少都有一定的损失,速度上也没有达到极致。因此今后量化是一个不错的方向,值得一试。

相关实践学习
【AI破次元壁合照】少年白马醉春风,函数计算一键部署AI绘画平台
本次实验基于阿里云函数计算产品能力开发AI绘画平台,可让您实现“破次元壁”与角色合照,为角色换背景效果,用AI绘图技术绘出属于自己的少年江湖。
从 0 入门函数计算
在函数计算的架构中,开发者只需要编写业务代码,并监控业务运行情况就可以了。这将开发者从繁重的运维工作中解放出来,将精力投入到更有意义的开发任务上。
相关文章
|
人工智能 缓存 并行计算
技术改变AI发展:Ada Lovelace架构解读及RTX 4090性能测试分析(系列三)
简介:随着人工智能(AI)的迅速发展,越来越多的应用需要巨大的GPU计算资源。Ada lovelace(后面简称Ada)是NVIDIA最新的图形处理器架构,随2022年9月20日发布的RTX 4090一起公布。
142465 62
技术改变AI发展:Ada Lovelace架构解读及RTX 4090性能测试分析(系列三)
|
并行计算 TensorFlow 调度
推荐场景GPU优化的探索与实践:CUDA Graph与多流并行的比较与分析
RTP 系统(即 Rank Service),是一个面向搜索和推荐的 ranking 需求,支持多种模型的在线 inference 服务,是阿里智能引擎团队沉淀多年的技术产品。今年,团队在推荐场景的GPU性能优化上又做了新尝试——在RTP上集成了Multi Stream,改变了TensorFlow的单流机制,让多流的执行并行,作为增加GPU并行度的另一种选择。本文详细介绍与比较了CUDA Graph与多流并行这两个方案,以及团队的实践成果与心得。
|
机器学习/深度学习 自然语言处理 并行计算
Self-Attention 原理与代码实现
Self-Attention 原理与代码实现
824 0
|
4月前
|
人工智能 JSON 运维
🚀🚀 【MCP + AI】grafana-mcp-analyzer:基于 MCP 的轻量图表分析助手
`grafana-mcp-analyzer` 是一个开源项目,通过 MCP 协议连接 AI 助手与 Grafana,实现智能分析监控数据。只需简单配置,AI 可快速解读图表,提供性能瓶颈、优化建议等专业分析,极大提升运维效率。支持多种数据源(Prometheus、ES 等),适配 ChatGPT、Claude 等模型,部署轻量,操作便捷。从此告别深夜手动排查问题,让 AI 成为你的智能运维专家!项目地址:<https://github.com/SailingCoder/grafana-mcp-analyzer>
562 1
🚀🚀 【MCP + AI】grafana-mcp-analyzer:基于 MCP 的轻量图表分析助手
|
11月前
|
机器学习/深度学习 人工智能 并行计算
【AI系统】Tensor Core 基本原理
本文深入介绍了英伟达GPU中的Tensor Core,一种专为加速深度学习设计的硬件单元。文章从发展历程、卷积计算、混合精度训练及基本原理等方面,详细解析了Tensor Core的工作机制及其在深度学习中的应用,旨在帮助读者全面理解Tensor Core技术。通过具体代码示例,展示了如何在CUDA编程中利用Tensor Core实现高效的矩阵运算,从而加速模型训练和推理过程。
1558 0
|
10月前
|
人工智能 并行计算 程序员
【AI系统】SIMD & SIMT 与芯片架构
本文深入解析了SIMD(单指令多数据)与SIMT(单指令多线程)的计算本质及其在AI芯片中的应用,特别是NVIDIA CUDA如何实现这两种计算模式。SIMD通过单指令对多个数据进行操作,提高数据并行处理能力;而SIMT则在GPU上实现了多线程并行,每个线程独立执行相同指令,增强了灵活性和性能。文章详细探讨了两者的硬件结构、编程模型及硬件执行模型的区别与联系,为理解现代AI计算架构提供了理论基础。
1502 12
|
存储 Docker 容器
containerd容器运行时快速入门使用指南
关于containerd容器运行时的快速入门使用指南,涵盖了镜像管理、容器管理、NameSpace管理、数据持久化、镜像推送至Harbor仓库以及Docker与Containerd集成等内容。
1269 1
containerd容器运行时快速入门使用指南
|
机器学习/深度学习 人工智能 分布式计算
阿里云人工智能平台PAI论文入选OSDI '24
阿里云人工智能平台PAI的论文《Llumnix: Dynamic Scheduling for Large Language Model Serving》被OSDI '24录用。论文通过对大语言模型(LLM)推理请求的动态调度,大幅提升了推理服务质量和性价比。
|
Kubernetes 关系型数据库 网络架构
ray集群部署vllm的折磨
概括如下: 在构建一个兼容多种LLM推理框架的平台时,开发者选择了Ray分布式框架,以解决资源管理和适配问题。然而,在尝试集成vllm时遇到挑战,因为vllm内部自管理Ray集群,与原有设计冲突。经过一系列尝试,包括调整资源分配、修改vllm源码和利用Ray部署的`placement_group_bundles`特性,最终实现了兼容,但依赖于非官方支持的解决方案。在面对vllm新版本和Ray部署的`reconfigure`方法问题时,又需权衡和调整实现方式。尽管面临困难,开发者认为使用Ray作为统一底层仍具有潜力。
|
机器学习/深度学习 存储 并行计算
Pytorch自动混合精度(AMP)介绍与使用 - autocast和Gradscaler
Pytorch自动混合精度(AMP)介绍与使用 - autocast和Gradscaler
Pytorch自动混合精度(AMP)介绍与使用 - autocast和Gradscaler