一、前言
本文中根据甲方需求实现调用GPU
加速计算两千万次的余弦相似度计算,根据需求分析,可以大致分为两个实现目标。
- 在
10s
内完成1:2000000
次的余弦相似度计算。 - 在
10s
内同时完成topk
的计算。
二、实现方法
因为在需求中需要完成GPU
来加载数据和计算,因此使用普通的程序是无法完成调用GPU
的任务,因此,在实现过程中决定采用卷积神经网络的思想来完成数据的加载和计算,同时也能根据需求,分别测试CPU
和GPU
两种设备下的速度。
初次之外,本文在实现过程中分别采用了两种方式来完成,分别如下:
- 采用调用
torch
网络中的torch.nn.function.cosine_similarity
来计算余弦相似度 - 自行构造矩阵同时完成多纬度的计算。
经过测试,第一种方式中使用的for
循环严重导致了时间的加长,第二种方式则能分别实现数据的加载和计算,因此,选择第二种方式更加合理。
2.1、方式一:For循环
主要实现分为两步:
- 构造网络结构,本文中的网络实际仅为余弦相似度的计算。
class CosineSimilarity
为网络结构。 - 生成随机特征,并加载到
device
设备中。 - 进行计算并循环第二部分。
代码如下:
import torch
import torch.nn.functional as F
import time
class CosineSimilarity(torch.nn.Module):
def __init__(self, dim=1, eps=1e-8):
super(CosineSimilarity, self).__init__()
self.dim = dim
self.eps = eps
def forward(self, x1, x2):
return F.cosine_similarity(x1, x2, self.dim, self.eps)
def test():
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = CosineSimilarity().to(device)
model = torch.nn.DataParallel(model)
start_time = time.time()
for i in range(50000):
x1 = torch.randn(1, 256).to(device)
x2 = torch.randn(1, 256).to(device)
distance = model(x1, x2)
print("index:{}, similar:{}".format(i, distance))
end_time = time.time()
print("time is :{}".format(end_time-start_time))
if __name__ == "__main__":
test()
2.2、方式二:自行构造矩阵
与方式一大致想通过,不同的是第一种方式中每次同时生成两个256
纬的数据,加载到设备中之后再进行一次计算,第二种方式是直接全部加载数据到设备中再进行计算。基本过程与第一种方式类似。
- 构建网络模型,
CosineSimilarityTest
- 加载数据到设备中
CPU/GPU
- 加载模型到设备中
model = CosineSimilarityTest().to(device)
- 进行计算
import torch
import torch.nn.functional as F
import time
class CosineSimilarityTest(torch.nn.Module):
def __init__(self):
super(CosineSimilarityTest, self).__init__()
def forward(self, x1, x2):
x2 = x2.t()
x = x1.mm(x2)
x1_frobenius = x1.norm(dim=1).unsqueeze(0).t()
x2_frobenins = x2.norm(dim=0).unsqueeze(0)
x_frobenins = x1_frobenius.mm(x2_frobenins)
final = x.mul(1/x_frobenins)
return final
def main():
# 加载数据到设备中CP\GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
x1 = torch.randn(5000000, 256).to(device)
x2 = torch.randn(1, 256).to(device)
start_time = time.time()
model = CosineSimilarityTest().to(device)
# 同时需要多卡计算时需要
# model = torch.nn.DataParallel(model)
final_value = model(x1, x2)
print(final_value.size())
# 输出排序并输出topk的输出
value, indec = torch.topk(final_value, 3, dim=0, largest=True, sorted=True)
print(value)
end_time = time.time()
print("消耗时间为:{}".format(end_time - start_time))
if __name__ == "__main__":
main()
三、方法选择
通过上述两种方式的代码展现,可以直观的看出,第二种方式要更加的清晰。易于统计数据的加载和计算的消耗时间,而第一种方式明显更加混乱,加载数据与计算混合在一起。除此之外,还有如下两点原因。
- 使用
for
循环单次计算余弦相似度,明显循环调用了余弦相似度的计算公式,冗余严重。 - 第二种方式,采用矩阵的计算方式,更加符合
GPU
的架构设计。
四、测试结果
该测试结果方式2
在服务器上CPU
和GPU
设备的测试结果,和上述的实际代码实际输出有偏差,但测试结果相同。
测试结果中指标分别为:
1、加载数据时间。
2、输出的结果纬度尺寸。
3、topk的值,该测试的k值为3.
4、计算消耗时间。
1、CPU设备测试结果。
2、GPU设备测试结果
五、结果分析
从上述的方法二的结论中可以看出两点不同的地方。
CPU
加载数据的时间要明显比GPU
时间更短,也更加优秀GPU
更加适合处理矩阵之间的计算,在浮点矩阵的计算上GPU
比CPU
更久优秀。