【darknet源码解析-06】gemm.h和gemm.c解析

本系列为darknet源码解析,本次解析src/gemm.h 与src/gemm.c两个。在上一篇文章中,我们已经详细讲解了输入特征图如何进行转换,那么在本文中,gemm主要完成矢量和矩阵的加速运算,是darknet卷积底层实现的核心,其实也是caffe卷积实现的核心。

gemm.h 的包含的代码如下:主要就是两个函数的gemm,gemm_cpu的定义【gemm_bin暂不分析】,在这里我们先不涉及gpu那块,先讲解cpu这块的矩阵加速运算。

#ifndef GEMM_H
#define GEMM_H

void gemm_bin(int M, int N, int K, float ALPHA, 
        char  *A, int lda, 
        float *B, int ldb,
        float *C, int ldc);
        
void gemm(int TA, int TB, int M, int N, int K, float ALPHA, 
                    float *A, int lda, 
                    float *B, int ldb,
                    float BETA,
                    float *C, int ldc);

void gemm_cpu(int TA, int TB, int M, int N, int K, float ALPHA, 
        float *A, int lda, 
        float *B, int ldb,
        float BETA,
        float *C, int ldc);

#ifdef GPU
void gemm_gpu(int TA, int TB, int M, int N, int K, float ALPHA, 
        float *A_gpu, int lda, 
        float *B_gpu, int ldb,
        float BETA,
        float *C_gpu, int ldc);

void gemm_gpu(int TA, int TB, int M, int N, int K, float ALPHA, 
        float *A, int lda, 
        float *B, int ldb,
        float BETA,
        float *C, int ldc);
#endif
#endif

gemm.c 的详细分析如下,可以先看后面的白话总结描述以及小例子再来看源码:

#include "gemm.h"
#include "utils.h"
#include "cuda.h"
#include 
#include 
#include 

void gemm_bin(int M, int N, int K, float ALPHA, 
        char  *A, int lda, 
        float *B, int ldb,
        float *C, int ldc)
{
    int i,j,k;
    for(i = 0; i < M; ++i){
        for(k = 0; k < K; ++k){
            char A_PART = A[i*lda+k];
            if(A_PART){
                for(j = 0; j < N; ++j){
                    C[i*ldc+j] += B[k*ldb+j];
                }
            } else {
                for(j = 0; j < N; ++j){
                    C[i*ldc+j] -= B[k*ldb+j];
                }
            }
        }
    }
}

float *random_matrix(int rows, int cols)
{
    int i;
    float *m = calloc(rows*cols, sizeof(float));
    for(i = 0; i < rows*cols; ++i){
        m[i] = (float)rand()/RAND_MAX;
    }
    return m;
}

void time_random_matrix(int TA, int TB, int m, int k, int n)
{
    float *a;
    if(!TA) a = random_matrix(m,k);
    else a = random_matrix(k,m);
    int lda = (!TA)?k:m;
    float *b;
    if(!TB) b = random_matrix(k,n);
    else b = random_matrix(n,k);
    int ldb = (!TB)?n:k;

    float *c = random_matrix(m,n);
    int i;
    clock_t start = clock(), end;
    for(i = 0; i<10; ++i){
        gemm_cpu(TA,TB,m,n,k,1,a,lda,b,ldb,1,c,n);
    }
    end = clock();
    printf("Matrix Multiplication %dx%d * %dx%d, TA=%d, TB=%d: %lf ms\n",m,k,k,n, TA, TB, (float)(end-start)/CLOCKS_PER_SEC);
    free(a);
    free(b);
    free(c);
}

/**
 * gemm函数调用了gemm_cpu()函数,并且将参数原封不动的传给gemm_cpu()
 */
void gemm(int TA, int TB, int M, int N, int K, float ALPHA, 
        float *A, int lda, 
        float *B, int ldb,
        float BETA,
        float *C, int ldc)
{
    gemm_cpu( TA,  TB,  M, N, K, ALPHA,A,lda, B, ldb,BETA,C,ldc);
}


/**
 * 被gemm_cpu函数调用,实际完成 C = ALPHA * A * B + C 矩阵运算,输出的C也是按行存储(所有行并成一行)
 * @param M A,C的行数(不做转置)
 * @param N B,C的列数(不做装置)
 * @param K A的列数,C的行数(不做转置)
 * @param ALPHA 系数
 * @param A 输入矩阵(一维数组格式)
 * @param lda A的列数(不做转置)
 * @param B 输入矩阵(一维数组格式)
 * @param ldb B的列数(不做转置)
 * @param C 输入矩阵(一维数组格式)
 * @param ldc C的列数(不做转置)
 *
 * 说明:此函数在gemm_cpu()函数中调用,是其中四中情况之一,A不进行转置,B不进行转置
 *      函数名gemm_nt()中nt分别表示 not transpose, tranpose
 */
void gemm_nn(int M, int N, int K, float ALPHA, 
        float *A, int lda, 
        float *B, int ldb,
        float *C, int ldc)
{ // input: 矩阵A[M,K], filter: 矩阵B[K,N],  output: 矩阵C[M,N]
    int i,j,k;
    #pragma omp parallel for
    // 大循环:遍历A的每一行,i表示A的第i行,也是C的第i行
    for(i = 0; i < M; ++i){
        // 中循环:遍历每一行的所有列,k表示A的第k列,同时表示B的第k行
        for(k = 0; k < K; ++k){
                            // 先计算ALPHA * A (A中每一个元素乘以ALPHA)
            register float A_PART = ALPHA*A[i*lda+k];
            // 内循环:遍历B中所有列,每次大循环完毕,将计算得到A×B一行的结果
            // j是B的第j列,也是C的第j列
            for(j = 0; j < N; ++j){
                // A中第i行k列与B中第k行i列对应相乘,因为一个大循环要计算A×B一行的结果
                // 因此,这里用一个内循环,并没有直接乘以B[k*ldb+i]
                // 每个内循环完毕,将计算A×B整行的部分结果(A中第i行k列与B所有列第k行所有元素相乘的结果)
                C[i*ldc+j] += A_PART*B[k*ldb+j];
            }
        }
    }
}


/**
 * 被gemm_cpu()函数调用,实际完成 C = ALPHA * A * B^T + C 矩阵计算
 * @param M A,C的行数(不做转置)或者A^T的行数(做转置),此处A未转置,故为A的行数
 * @param N B,C的列数(不做转置)或者B^T的列数(做转置),此处B转置,故为B^T的列数
 * @param K  A的列数(不做转置)或者A^T的列数(做转置),B的行数(不做转置)或者B^T(做转置),此处A未转置,B转置,故为A的列数,B^T的行数
 * @param ALPHA 系数
 * @param A 输入矩阵
 * @param lda  A的列数(不做转置)或者A^T的行数(做转置),此处A未转置,故为A的列数
 * @param B 输入矩阵
 * @param ldb B的列数(不做转置)或者B^T的行数(做转置),此处B转置,故为B^T的行数
 * @param C 输入矩阵
 * @param ldc 矩阵C的列数
 * 说明:此函数在gemm_cpu()函数中调用,是其中四中情况之一,A不进行转置,B转置
 *      函数名gemm_nt()中nt分别表示 not transpose, tranpose
 */
void gemm_nt(int M, int N, int K, float ALPHA, 
        float *A, int lda, 
        float *B, int ldb,
        float *C, int ldc)
{// input: 矩阵A[M,K], filter: 矩阵B[K,N],  output: 矩阵C[M,N]
    int i,j,k;
    #pragma omp parallel for
    // 大循环:遍历A的每一行,i表示A的第i行,也是C的第i行
    for(i = 0; i < M; ++i){

        for(j = 0; j < N; ++j){
            register float sum = 0;
            //内循环:每次内循环结束,将计算A中第i行与B中第j列相乘的结果
            //也就是得到C[i][j],因为C也一维化,且按行存储,所以得到C[i*lda+j]
            // k表示A的第几列,也表示
            for(k = 0; k < K; ++k){
                sum += ALPHA*A[i*lda+k]*B[j*ldb + k];
            }
            C[i*ldc+j] += sum;
        }
    }
}

/**
 * 被gemm_cpu()函数调用,实际完成 C = ALPHA * A^T * B + C 矩阵计算
 * @param M A,C的行数(不做转置)或者A^T的行数(做转置),此处A转置,故为A^T的行数
 * @param N B,C的列数(不做转置)或者B^T的列数(做转置),此处B未转置,故为B的列数
 * @param K  A的列数(不做转置)或者A^T的列数(做转置),B的行数(不做转置)或者B^T行数(做转置),此处A未转置,B转置,故为A^T的列数,B的行数
 * @param ALPHA 系数
 * @param A 输入矩阵
 * @param lda  A的列数(不做转置)或者A^T的行数(做转置),此处A转置,故为A^T的行数
 * @param B 输入矩阵
 * @param ldb B的列数(不做转置)或者B^T的行数(做转置),此处B未转置,故为B的列数
 * @param C 输入矩阵
 * @param ldc 矩阵C的列数
 * 说明:此函数在gemm_cpu()函数中调用,是其中四中情况之一,A进行转置,B不进行转置
 *      函数名gemm_tn()中tn分别表示  tranpose,not transpose
 */
void gemm_tn(int M, int N, int K, float ALPHA, 
        float *A, int lda, 
        float *B, int ldb,
        float *C, int ldc)
{
    int i,j,k;
    #pragma omp parallel for
    for(i = 0; i < M; ++i){
        for(k = 0; k < K; ++k){
            register float A_PART = ALPHA*A[k*lda+i];
            for(j = 0; j < N; ++j){
                C[i*ldc+j] += A_PART*B[k*ldb+j];
            }
        }
    }
}


/**
 * 被gemm_cpu()函数调用,实际完成 C = ALPHA * A^T  * B^T + C 矩阵计算
 * @param M A,C的行数(不做转置)或者A^T的行数(做转置),此处A转置,故为A^T的行数
 * @param N B,C的列数(不做转置)或者B^T的列数(做转置),此处B转置,故为B^T的列数
 * @param K  A的列数(不做转置)或者A^T的列数(做转置),B的行数(不做转置)或者B^T(做转置),此处A转置,B转置,故为A^T的列数,B^T的行数
 * @param ALPHA 系数
 * @param A 输入矩阵
 * @param lda  A的列数(不做转置)或者A^T的行数(做转置),此处A转置,故为A^T的行数
 * @param B 输入矩阵
 * @param ldb B的列数(不做转置)或者B^T的行数(做转置),此处B转置,故为B^T的行数
 * @param C 输入矩阵
 * @param ldc 矩阵C的列数
 * 说明:此函数在gemm_cpu()函数中调用,是其中四中情况之一,A进行转置,B进行转置
 *      函数名gemm_tt()中tt分别表示 transpose, tranpose
 */
void gemm_tt(int M, int N, int K, float ALPHA, 
        float *A, int lda, 
        float *B, int ldb,
        float *C, int ldc)
{
    int i,j,k;
    #pragma omp parallel for
    for(i = 0; i < M; ++i){
        for(j = 0; j < N; ++j){
            register float sum = 0;
            for(k = 0; k < K; ++k){
                sum += ALPHA*A[i+k*lda]*B[k+j*ldb];
            }
            C[i*ldc+j] += sum;
        }
    }
}



/**
 * 矩阵计算,完成C = ALPHA * A * B + BETA * C 矩阵计算,最后的输出为C
 * @param TA 是否需要对A做转置操作,是为1,否为0(要不要转置取决于A,B之间的维度是否匹配,比如A:3*2, B:4*2, 则需要对B转置,才满足矩阵乘法)
 * @param TB 同上
 * @param M A,C 的行数(若A需要转置,则此出给出转置后A即A^T的行数,而不是转置前的)
 * @param N B,C 的列数(若B需要转置,则此处给出转置后B即B^T的列数,而不是转置前的)
 * @param K A的列数,B的行数(同样,若A与B中的二者或者其中一个需要转置,则不管怎么样,转置后的A,B必须行列能够匹配,符合矩阵乘法规则,K也是转置后的值,不是转置的)
 * @param ALPHA 系数
 * @param A 输入矩阵
 * @param lda A的列数(不做转置)或者行数(做转置,且给的是转置后A即A^T的行数)
 * @param B 输入矩阵
 * @param ldb B的列数(不做转置)或者行数(做转置,且给的是转置后B即B^T的行数)
 * @param BETA 系数
 * @param C 输入矩阵
 * @param ldc C的列数
 */
void gemm_cpu(int TA, int TB, int M, int N, int K, float ALPHA, 
        float *A, int lda, 
        float *B, int ldb,
        float BETA,
        float *C, int ldc)
{
    //printf("cpu: %d %d %d %d %d %f %d %d %f %d\n",TA, TB, M, N, K, ALPHA, lda, ldb, BETA, ldc);
    int i, j;
    // 先行计算BETA * C,并把结果存入C中,得到C将为M行N列(按行存储在一维数组中)
    for(i = 0; i < M; ++i){
        for(j = 0; j < N; ++j){
            C[i*ldc + j] *= BETA;
        }
    }
    if(!TA && !TB) // TA = 0, TB = 0,
        gemm_nn(M, N, K, ALPHA,A,lda, B, ldb,C,ldc);
    else if(TA && !TB) // TA = 1, TB = 0
        gemm_tn(M, N, K, ALPHA,A,lda, B, ldb,C,ldc);
    else if(!TA && TB) // TA = 0, TB = 1
        gemm_nt(M, N, K, ALPHA,A,lda, B, ldb,C,ldc);
    else // TA = 1, TB = 1
        gemm_tt(M, N, K, ALPHA,A,lda, B, ldb,C,ldc);
}



#ifdef GPU

#include 


/**
 * 矩阵计算GPU实现,调用CUDA中cublasSgemm()函数完成 C_gpu = ALPHA + A_gpu * B_gpu + BETA * C_gpu的线性矩阵运算,
 * 与gemm_cpu()基本类似,输入参数也基本相同,但是存在两点不同:
 * 1. 此处是直接调用CUDA cuBLAS库中的cublasSgemm()函数进行矩阵运算,而无需gemm_cpu()那样,需要自己用循环挨个元素相乘实现;
 * 2. 在GPU中,默认采用的矩阵存储格式是按列存储,而不是我们之前一度习惯的按行存储,此处调用的cublasSgemm()也不例外,
 *    所以下面会有一些不同的操作(由于这个原因,相比于cpu版本的gemm_cpu(),又要复杂一些)
 *
 *
 *
 *
 * GPU使用cuBLAS库中cublasSgemm()函数进行矩阵乘法计算,参看:
 * 这个网址是CUDA关于cuBLAS库的官方文档,此处cublasSgemm()函数在2.7.1节: cublasgemm();
 * 可以看出cublasSgem()函数完成C_gpu = ALPHA * A_gpu * B_gpu + BETA * C_gpu的线性矩阵计算
 *
 * @param TA 是否需要对A做转置操作,是为1,否为0(要不要转置取决于A,B之间的维度是否匹配,比如A:3*2, B:4*2, 则需要对B转置,才满足矩阵乘法)
 * @param TB 同上
 * @param M A,C 的行数(若A需要转置,则此出给出转置后A即A^T的行数,而不是转置前的)
 * @param N B,C 的列数(若B需要转置,则此处给出转置后B即B^T的列数,而不是转置前的)
 * @param K A的列数,B的行数(同样,若A与B中的二者或者其中一个需要转置,则不管怎么样,转置后的A,B必须行列能够匹配,符合矩阵乘法规则,K也是转置后的值,不是转置的)
 * @param ALPHA 系数
 * @param A_gpu 输入矩阵,且其内存在GPU设备内存中,不在主机内存中(由cudaMalloc分配,由cudaFree释放)
 * @param lda A的列数(不做转置)或者行数(做转置,且给的是转置后A即A^T的行数)
 * @param B_gpu 输入矩阵,且其内存在GPU设备内存中,不在主机内存中(由cudaMalloc分配,由cudaFree释放)
 * @param ldb B的列数(不做转置)或者行数(做转置,且给的是转置后B即B^T的行数)
 * @param BETA 系数
 * @param C_gpu 输入矩阵,且其内存在GPU设备内存中,不在主机内存中(由cudaMalloc分配,由cudaFree释放)
 * @param ldc C的列数
 *
 * 可以看出,如果不是因为存储方式的不同,cublasSgemm()函数的结构也与darknet自己实现的cpu版本的gemm_cpu一模一样;
 * 因为二者存储格式不同,需要交换A_gpu, B_gpu的位置,对应M和N之间,TB与TA之间,ldb与lda之间都要相互交换;
 *
 */
void gemm_gpu(int TA, int TB, int M, int N, int K, float ALPHA, 
        float *A_gpu, int lda, 
        float *B_gpu, int ldb,
        float BETA,
        float *C_gpu, int ldc)
{
    //根据官网,这个变量是一个对开发者不透明的变量,也就是里面聚义包给什么,开发这一般无法知道,
    //只知道里面包含的cuBLAS库的相关信息,且这个变量是必须的,按照官网的描述,CUBLAS库中所有的函数都需要这个变量参数
    //(且都是作为第一个参数),该变量由cublasCreate()初始化,并由cuBLASDestroy()销毁。
    cublasHandle_t handle = blas_handle();

    /* cublasSgemm()函数输入参数说明
     * @param handle
     * @param transa 是否需要转置A_gpu, 这里transa = TB ? CUBLAS_OP_T : CUBLAS_OP_N (是个条件表达式),如果TB =1,
     *               则取CUBLAS_OP_T,即需要对A_gpu转置;
     * @param transb 是否需要转置A_gpu, 这里transa = TA ? CUBLAS_OP_T : CUBLAS_OP_N (是个条件表达式),如果TA =1,
     *               则取CUBLAS_OP_T,即需要对B_gpu转置;
     * @param M A_gpu,C_gpu 的行数(若A_gpu需要转置,则此出给出转置后A_gpu即A_gpu^T的行数,而不是转置前的)
     * @param N B_gpu,C_gpu 的列数(若B_gpu需要转置,则此处给出转置后B_gpu即B_gpu^T的列数,而不是转置前的)
     * @param K A_gpu的列数,B_gpu的行数(同样,若A_gpu与B_gpu中的二者或者其中一个需要转置,则不管怎么样,转置后的A_gpu,B_gpu必须
     *          行列能够匹配,符合矩阵乘法规则,K也是转置后的值,不是转置的)
     * @param ALPHA 实数系数
     * @param B_gpu 输入矩阵
     * @param ldb B_gpu的列数(不做转置)或者行数(做转置,且传入的是转置后B_gpu即B_gpu^T的行数)
     * @param A_gpu 输入矩阵
     * @param lda A_gpu的列数(不做转置)或者行数(做转置,且传入的是转置后A_gpu即A_gpu^T的行数)
     * @param BETA 实数系数
     * @param C_gpu 计算结果
     * @param ldc C_gpu的列数
     *
     */
    cudaError_t status = cublasSgemm(handle, (TB ? CUBLAS_OP_T : CUBLAS_OP_N),
            (TA ? CUBLAS_OP_T : CUBLAS_OP_N), N, M, K, &ALPHA, B_gpu, ldb, A_gpu, lda, &BETA, C_gpu, ldc);
            // 检查cublasSgemm运算是否正常
    check_error(status);
}

#include 
#include 
#include 
#include 

void time_gpu_random_matrix(int TA, int TB, int m, int k, int n)
{
    float *a;
    if(!TA) a = random_matrix(m,k);
    else a = random_matrix(k,m);
    int lda = (!TA)?k:m;
    float *b;
    if(!TB) b = random_matrix(k,n);
    else b = random_matrix(n,k);
    int ldb = (!TB)?n:k;

    float *c = random_matrix(m,n);
    int i;
    clock_t start = clock(), end;
    for(i = 0; i<32; ++i){
        gemm_gpu(TA,TB,m,n,k,1,a,lda,b,ldb,1,c,n);
    }
    end = clock();
    printf("Matrix Multiplication %dx%d * %dx%d, TA=%d, TB=%d: %lf s\n",m,k,k,n, TA, TB, (float)(end-start)/CLOCKS_PER_SEC);
    free(a);
    free(b);
    free(c);
}

void time_gpu(int TA, int TB, int m, int k, int n)
{
    int iter = 10;
    float *a = random_matrix(m,k);
    float *b = random_matrix(k,n);

    int lda = (!TA)?k:m;
    int ldb = (!TB)?n:k;

    float *c = random_matrix(m,n);

    float *a_cl = cuda_make_array(a, m*k);
    float *b_cl = cuda_make_array(b, k*n);
    float *c_cl = cuda_make_array(c, m*n);

    int i;
    clock_t start = clock(), end;
    for(i = 0; i

其实,gemm总结起来就完成一个矩阵乘法的运算:C = ALPHA * A * B + BETA * C

上述公式中,A,B,C为矩阵,A,B为输入矩阵,C矩阵保存运算结果。ALPHA,BETA为系数。这样看起来是不是很简单,接下来我们需要考虑矩阵A,B的行数和列数分别是多少,这里我们假设矩阵A为[M,K],矩阵B为[K,N],那么矩阵C为[M,N]。我们都直到矩阵A,B,C在逻辑是一个二维的结果,在这里实际的存储结构是一个一维数组,按行存储。

接下来,我们来具体看一个例子,为了方便运算,我们这里假设ALPHA=1,BETA=0。实际上我们只对A*B进行运算,我们分为四种情况进行讨论,为什么要分为四种情况呢?其实就是引入矩阵的转置。

1. A * B

【darknet源码解析-06】gemm.h和gemm.c解析_第1张图片

 

【darknet源码解析-06】gemm.h和gemm.c解析_第2张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第3张图片

 

【darknet源码解析-06】gemm.h和gemm.c解析_第4张图片

 综合计算一下,矩阵C的内容如下:

 

【darknet源码解析-06】gemm.h和gemm.c解析_第5张图片

2. A^T * B

【darknet源码解析-06】gemm.h和gemm.c解析_第6张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第7张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第8张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第9张图片

 

 其实这跟A* B h很想象。

 

3. A * B^T

【darknet源码解析-06】gemm.h和gemm.c解析_第10张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第11张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第12张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第13张图片

 综合一下,便可以得到结果

 

 

4 A^T * B^T

【darknet源码解析-06】gemm.h和gemm.c解析_第14张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第15张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第16张图片

【darknet源码解析-06】gemm.h和gemm.c解析_第17张图片

 跟A * B^T很想象。

 ok,此时你再取看源码豁然开朗。

gemm_nn 函数就是计算 A * B这种类型;

gemm_tn 函数就是计算A^T * B这种类型;

gemm_nt 函数就是计算A * B^T这种类型;

gemm_tt 函数就是计算A^T * B^T 这种类型;

gemm_cpu 函数就是根据矩阵A和B的情况来实际调用 gemm_nn 、gemm_tn 、gemm_nt、gemm_tt 函数;

gemm 函数其实就是在gemm_cpu函数上再封装一层,参数原封不动传递给gemm_cpu函数;

完,

你可能感兴趣的:(darknet源码解析)