背景

搜索場景下通常有少量的查詢向量和大量的索引向量求距離,不管是L2距離還是cos距離都和這兩者的內積有關係, 所以本文就來看看Faiss中內積的GPU實現,內積計算本質上是矩陣相乘,Faiss並沒有自己寫kernel,而是調用了大名鼎鼎的cublas,下面來看些細節,本文不會講cublas的實現,而是重點講在faiss中是如何調用cublas的

cublass的奇怪之處

輸入

我們來看一個矩陣,以一個4*2的舉證為例, 在程序裡面,矩陣存儲在一個一維數組中,總共8個元素

[x0,x1,x2,x3,x4,x5,x6,x7]

邏輯上我們認為是這樣的一個矩陣

[
x0 x1
x2 x3
x4,x5
x6,x7
]

但如果放入cublas, 就會成為這樣的形式

[
x0 x2 x4 x6
x1 x3 x5 x7
]

導致這個差別的主要原因是cublas在填充矩陣時,是按照列的順序來填充的,而我們是通常是按行

如果這樣放入,就和我們的意願違背了

輸出

當我們計算完一個數組,並拷貝回主機的時候,也會發現也這樣

假設輸出同樣邏輯上是4*2矩陣

[
x0 x1
x2 x3
x4,x5
x6,x7
]

那麼輸出是[x0,x2,x4,x6,x1,x3,x5,x7]

那麼我們會認為輸出是

[
x0 x2
x4 x6
x1 x3
x5 x7
]

輸出也是按列填充,實際上這也不是我麼想要的

怎麼解

看看我們的目標

查詢向量矩陣(Q): 3個2維的向量
索引向量矩陣(D): 4個2維的向量
結果(R): 3*4的結果矩陣

即 ? Q*D^T=R ? [式1]

正確的輸出

我們想要正確的輸出R(3*4), 那麼cublas中就不能是3 *4 的R,否則就會像上面的例子那樣輸出有問題,我們應該在cublas中輸出的是啥?

比如我們想輸出

[
R00 R01 R02 R03
R10 R11 R12 R13
R20 R21 R22 R23
]

如果想要這個那麼反推在cublas中的結果矩陣應該是

[
R00 R10 R20
R01 R11 R21
R02 R12 R22
R03 R13 R23
]

這實際上就是? R^T

也就是說cublas中的? R^T , 拷貝到主機上就變成了 R

正確的輸入

既然我們要得到? ? R^T

將[式1]全部全部轉置,就得到

D*Q^T=R^T [式2]

那麼我們如何正確的輸入? D 和? Q^T

以?? D為例

正如上文提到,如果直接輸入? D?,那麼結果是不對的

也正如上面的輸出例子所示,如果想在cublas中的正確的? D,那麼實際的輸入應該是? D^T

................????????????????????有點暈了,因為cublas這個奇怪的特性,導致繞來繞去

還是先把這個? D的例子搞清楚

我們希望在cublas中得到? (4*2)

[
D00 D01
D10 D11
D20 D21
D30 D31
]

那麼我們的輸入應該

[D00 D10 D20 D30 D01 D11 D21 D31] (如上提到,cublas是按列填充的)

那麼我們可以認為這實際就是? D^T (2*4) 即

[
D00 D10 D20 D30
D01 D11 D21 D31
]

同理可以認為要得到? Q^T ,直接輸入 Q 就可以了

休息下,我們稍微總結下

  • 我們一定要分清,cublas中的矩陣(GPU設備上的參與計算的矩陣)和cublas外的矩陣(即我們主機端的輸入和輸出矩陣)
  • 我們必須看到公式中的矩陣實際上屬於前者
  • 由於cublas特殊的按列填充,會使得cublas內和cublas外的矩陣有天然的轉置關係

如下圖

參考代碼

T* pA = transC ? a.data() : b.data();
T* pB = transC ? b.data() : a.data();
T* pC = c.data();
?
int m = c.getSize(1); // stride 1 size
int n = c.getSize(0); // other size
int k = transA ? a.getSize(0) : a.getSize(1);
?
int lda = transC ? a.getStride(0) : b.getStride(0);
int ldb = transC ? b.getStride(0) : a.getStride(0);
int ldc = c.getStride(0);
?
auto gemmTrA = transB ? CUBLAS_OP_T : CUBLAS_OP_N;
auto gemmTrB = transA ? CUBLAS_OP_T : CUBLAS_OP_N;
?
......
?
auto err = CublasGemm<T>::gemm(handle,
gemmTrA, gemmTrB,
m, n, k, alpha,
pA, lda, pB, ldb, beta,
pC, ldc, useHgemm);

基本上的原因都在上文中解釋過了,不過還是再稍微解釋下

  • pA 指向了b, 即上文提到D, 且需要轉置gemmTrA 為CUBLAS_OP_T
  • pB 指向了a, 即上文中提到的Q, 無需轉置gemmTrB為CUBLAS_OP_N
  • m,n,k 分別是4,3,2 即正在進行的矩陣計算為(4,2) * (2,3) = (4,3) 和上文中的式2對應起來了
  • lda,ldb,ldc 分別為2,2,4 代表了各自矩陣的行或者列數(因為pA轉置了,按列數計算,pB,pC未轉置按行計算,這和實際的輸入對應起來的即D,Q,R的實際列數)
  • 最後的就是cublas的矩陣計算函數CublasGemm<T>::gemm

最後附上完整測試代碼

#include <faiss/gpu/StandardGpuResources.h>
?
#include <faiss/gpu/utils/DeviceTensor.cuh>
#include <faiss/gpu/utils/MatrixMult.cuh>
#include <faiss/gpu/utils/CopyUtils.cuh>
?
#include <iostream>
#include <random>
#include <algorithm>
?
?
void calc(float *d_h,float *q_h,int d_n,int q_n, int dim,float *y_h){
//init gpu resource(memory && stream)
int dev = faiss::gpu::getCurrentDevice();
faiss::gpu::StandardGpuResources gpuResources;
?
auto stream = gpuResources.getDefaultStream(dev);
?
//copy from cpu to gpu
auto d_d_tmp = faiss::gpu::toDevice<float, 2>(&gpuResources, dev, d_h, stream, {d_n, dim});
faiss::gpu::Tensor<float, 2, true> d_d(d_d_tmp.data(),d_d_tmp.sizes());
?
auto q_d_tmp = faiss::gpu::toDevice<float, 2>(&gpuResources, dev, q_h, stream, {q_n, dim});
faiss::gpu::Tensor<float, 2, true> q_d(q_d_tmp.data(),q_d_tmp.sizes());
?
?
//init gpu output
faiss::gpu::DeviceTensor<float, 2, true> y_d_tmp({q_n,d_n});
faiss::gpu::Tensor<float, 2, true> y_d(y_d_tmp.data(), y_d_tmp.sizes());
?
//compute
faiss::gpu::runMatrixMult(y_d, false,
q_d, false,
d_d,true,
1.0f, 0.0f, false,
gpuResources.getBlasHandleCurrentDevice(),
stream);
?
//copy from gpu to cpu
faiss::gpu::fromDevice<float,2>(y_d, y_h, stream);
?
}
?
void run(int d_n,int q_n,int dim){
if(d_n<1 || q_n <1){
return;
}
?
std::random_device rd; // 將用於獲得隨機數引擎的種子
std::mt19937 gen(rd()); // 以 rd() 播種的標準 mersenne_twister_engine
std::uniform_real_distribution<float> dis(0, 1);
?
// init input data
float *d_h = new float[d_n * dim];
for(int i = 0; i < d_n; i++) {
for(int j = 0; j < dim; j++)
d_h[dim * i + j] = dis(gen);
}
?
float *q_h = new float[q_n * dim];
for(int i = 0; i < q_n; i++) {
for(int j = 0; j < dim; j++)
q_h[dim * i + j] = dis(gen);
}
?
//init output data
float *y_h = new float[q_n*d_n];
?
calc(d_h,q_h,d_n,q_n,dim,y_h);
?
int d_n_display=std::min(d_n,10);
int q_n_display=std::min(q_n,10);
?
//display result
for(int i = 0; i <q_n_display; i++) {
for(int j = 0; j < dim; j++){
std::cout <<q_h[dim * i + j]<<",";
}
std::cout <<std::endl;
?
}
?
std::cout <<"====="<<std::endl;
?
for(int i = 0; i <d_n_display; i++) {
for(int j = 0; j < dim; j++){
std::cout <<d_h[dim * i + j]<<",";
}
std::cout <<std::endl;
?
}
?
std::cout <<"====="<<std::endl;
for(int i = 0; i <d_n_display*q_n_display; i++) {
std::cout <<y_h[i]<<",";
}
std::cout <<std::endl;
}
?
int main() {
?
int d_n = 4;
int q_n = 3;
int dim = 2;
?
run(d_n,q_n,dim);
?
}

小結

  • 向量的內積使用cublas的矩陣相乘函數
  • 注意cublas的矩陣按列填充特性
  • 注意分清cublas中的矩陣和之外的矩陣的差異性以及仔細設計輸入和輸出

推薦閱讀:

相關文章