用CUDA實(shí)現(xiàn)一個稀疏的Logistic Regression

最近在研究Mxnet肤京,準(zhǔn)備從底層先嘗試一遍GPU的編程篙贸,這樣更容易熟悉mshadow的那部分邏輯投队。于是用CUDA嘗試了一下寫一個稀疏的Logistic Regression的程序。所謂稀疏的LR爵川,指的是特征非常多敷鸦,而每個樣本的特征非常少。因?yàn)槭菍W(xué)習(xí)CUDA寝贡,所以我構(gòu)造了一個問題扒披,假設(shè)特征的個數(shù)是100W,而每個樣本的特征只有100個左右圃泡。如果樣本的特征ID中偶數(shù)的多于奇數(shù)的碟案,我們就認(rèn)為是正樣本,否則是負(fù)樣本颇蜡。如下的代碼用來產(chǎn)生一個樣本:

void mock_sample(const int max_feature_id, vector< pair<int, float> > & out, int * label) {
  int count = rand() % 100 + 10;
  int ret = 0;
  for(int i = 0; i < count; i++) {
    int fid = rand() % max_feature_id;
    if(fid % 2 == 0) ret += 1;
    else ret -= 1;
    if(abs(ret) > 10) break;
    out.push_back(make_pair<int, float>(fid, 1.0));
  }
  *label = (ret > 0) ? 1 : 0;
}

LR的解法有很多種价说,PRML那本書上就提到了不下5種LR的優(yōu)化方法辆亏。因?yàn)橹皇菫榱藢W(xué)習(xí)CUDA,我選擇了最簡單的一種鳖目,也就是基于mini-batch的梯度下降法扮叨。LR的優(yōu)化核心主要是去學(xué)習(xí)w矩陣。而所謂稀疏性來自于計(jì)算點(diǎn)積<w, x>领迈,這里w是一個100W維的稠密向量甫匹,而x是一個100維左右的稀疏向量。因此<w,x>就是一個稠密向量和稀疏向量的點(diǎn)擊惦费。如果我們考慮mini-batch, 假設(shè)X是由n個樣本組成的一個batch,那么X就是一個n*100W的稀疏矩陣抢韭。而我們需要計(jì)算Xw就是一個稀疏矩陣和稠密向量的乘法薪贫。

為了解決這個乘法,我們第一個想到的是用 cusparse刻恭,這是由CUDA提供的一個稀疏矩陣的計(jì)算庫瞧省,在level2的函數(shù)中有CSR格式的稀疏矩陣和稠密向量的乘法:

cusparseStatus_t cusparseScsrmv(cusparseHandle_t handle, 
        cusparseOperation_t transA, int m, int n, int nnz, 
        const float *alpha, const cusparseMatDescr_t descrA, 
        const float *csrValA, const int *csrRowPtrA, 
        const int *csrColIndA, const float *x, 
        const float *beta, float *y)

我首先用cusparse實(shí)現(xiàn)了一個版本,具體代碼見 dot_cusparse.cu鳍贾。不過很不幸的是實(shí)現(xiàn)出來的速度還不如CPU的版本鞍匾。所以我也沒繼續(xù)做優(yōu)化,就放棄了CUSPARSE骑科,準(zhǔn)備完全自己實(shí)現(xiàn)橡淑。(按照后來的經(jīng)驗(yàn),如果繼續(xù)優(yōu)化咆爽,是有可能優(yōu)化的比較快的梁棠,所以讀者可以試一試)。

后來我想斗埂,其實(shí)如果用COO的表示方法符糊,其實(shí)可以很簡單的直接實(shí)現(xiàn)Xw的乘法,代碼如下:

__global__ void dot(float * val, int *row_ind, int *col_ind, int nnz, float * ret, float * w) {
  const int tid = (blockIdx.x * blockDim.x) + threadIdx.x;
  if (tid < nnz) {
    int r = row_ind[tid];
    int c = col_ind[tid];
    float v = val[tid];
    atomicAdd(&ret[r], v * w[c]);
  }
}

這里呛凶,val, row_ind, col_ind 是矩陣X的COO表示:

X[row_ind[i]][col_ind[i]] = val[i]
nnz = len(val) 是X中非0元素的個數(shù)

ret是dot的返回男娄,也就是Xw的計(jì)算結(jié)果,是1個n維的向量漾稀,下面一步就是計(jì)算這個n維向量中每一個元素的sigmoid:

__global__ void vec_sigmoid(float * d, int num) {
  const int tid = (blockIdx.x * blockDim.x) + threadIdx.x;
  if (tid < num) {
    if(d[tid] > 10.0) d[tid] = 1.0;
    else if(d[tid] < -10.0) d[tid] = 0.0;
    else d[tid] = 1.0 / (1.0 + exp(-1.0 * d[tid]));
  }
}

最后一步就是更新w向量模闲,這里我們不考慮正則化,函數(shù)如下:

__global__ void grad(float * val, int * row_ind, int *col_ind, float * mat_err,
             int nnz, float *act, float *label, 
             float *w, float learning_rate) {
  const int tid = (blockIdx.x * blockDim.x) + threadIdx.x;
  if (tid < nnz) {
    int r = row_ind[tid];
    int c = col_ind[tid];
    float v = val[tid];
    //mat_err是為了后面打印訓(xùn)練集的誤差而紀(jì)錄的
    mat_err[tid] = abs(label[r] - act[r]);
    float err = v * (label[r] - act[r]);
    atomicAdd(&w[c], learning_rate * err);
  }
}

整個LR的函數(shù)如下:

void lr(const vector< vector< pair<int, float> > > & data, 
    const vector<float> & label,
    CooMatrixHost * coo_mat_host, 
    CooMatrix * coo_mat,
    float * w, int ncol, int batch) {
  vec2coo(data, coo_mat_host, coo_mat);
  CUDA_CALL(cudaMemcpyAsync(coo_mat->label, label.data(), sizeof(float) * label.size(), cudaMemcpyHostToDevice, stream));
  CUDA_CALL(cudaMemset(coo_mat->act, 0, sizeof(float) * data.size()));

  int shared_memory_usage = 1;
  int num_blocks = ((coo_mat->nnz + (NUM_THREADS - 1)) / NUM_THREADS);
  dot<<<num_blocks, NUM_THREADS, shared_memory_usage, stream>>>(coo_mat->val,
                                coo_mat->row_ind,
                                coo_mat->col_ind,
                                coo_mat->nnz, 
                                coo_mat->act, w);
  
  num_blocks = ((data.size() + (NUM_THREADS - 1)) / NUM_THREADS);
  vec_sigmoid<<<num_blocks, NUM_THREADS, shared_memory_usage, stream>>>(coo_mat->act, data.size());
  
  num_blocks = ((coo_mat->nnz + (NUM_THREADS - 1)) / NUM_THREADS);
  grad<<<num_blocks, NUM_THREADS, shared_memory_usage, stream>>>(coo_mat->val,
                                 coo_mat->row_ind,
                                 coo_mat->col_ind,
                                 coo_mat->err,
                                 coo_mat->nnz, 
                                 coo_mat->act,
                                 coo_mat->label, 
                                 w, 0.01);
  if (batch % 10000 == 0){
    float * err = (float*) malloc(sizeof(float) * coo_mat->nnz);
    CUDA_CALL(cudaMemcpyAsync(err, coo_mat->err, sizeof(float) * coo_mat->nnz, cudaMemcpyDeviceToHost, stream));
    float total = 0.;
    for(int i = 0; i < coo_mat->nnz; i++) total += err[i];
    cout << total / (float) coo_mat->nnz << endl;
  }
}

整個程序并不復(fù)雜县好,所有代碼見這里围橡。這里也沒有考慮多線程和多卡。其中優(yōu)化的點(diǎn)有幾個:

1. 樣本是在CPU內(nèi)存中生成的缕贡,而計(jì)算是在GPU中完成的翁授。所以就涉及到CPU向GPU內(nèi)存拷貝的問題拣播。我們是在vec2coo函數(shù)中完成這一步的。
2. 因?yàn)椴豢紤]多線程和多卡收擦,因此CPU的內(nèi)存可以預(yù)先分配好(zeroCooMatrixHost)贮配。GPU的內(nèi)存也可以事先分配好(zeroCooMatrix)。否則malloc和cudaMalloc將是最耗時(shí)的函數(shù)塞赂。
3. 使用stream泪勒,cudaMemoryAsync。

這里還有一些沒有考慮到的優(yōu)化點(diǎn)宴猾,后面需要再試試圆存。

1. 多線程
2. 多個stream
3. 多個卡

因?yàn)閯傞_始寫CUDA的程序,讀者發(fā)現(xiàn)這個代碼有任何問題請發(fā)issue告訴我仇哆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沦辙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子讹剔,更是在濱河造成了極大的恐慌油讯,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件延欠,死亡現(xiàn)場離奇詭異陌兑,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)由捎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門兔综,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人隅俘,你說我怎么就攤上這事邻奠。” “怎么了为居?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵碌宴,是天一觀的道長。 經(jīng)常有香客問我蒙畴,道長贰镣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任膳凝,我火速辦了婚禮碑隆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蹬音。我一直安慰自己上煤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布著淆。 她就那樣靜靜地躺著劫狠,像睡著了一般拴疤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上独泞,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天呐矾,我揣著相機(jī)與錄音,去河邊找鬼懦砂。 笑死蜒犯,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的荞膘。 我是一名探鬼主播罚随,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼羽资!你這毒婦竟也來了毫炉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤削罩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后费奸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弥激,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年愿阐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了微服。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡缨历,死狀恐怖以蕴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辛孵,我是刑警寧澤丛肮,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站魄缚,受9級特大地震影響宝与,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冶匹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一习劫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嚼隘,春花似錦诽里、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灸眼。三九已至,卻和暖如春豌汇,著一層夾襖步出監(jiān)牢的瞬間幢炸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工拒贱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宛徊,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓逻澳,卻偏偏與公主長得像闸天,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子斜做,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容