本文结构
0 本文主体内容与行文组织
本文使用gRPC框架简单实现了CPU-FPGA的异构系统关于矩阵乘法的运算,通过一个小的benchmark
我们可以很直观地看到让具有特性的硬件去完成相关的运算,可以高效提升我们运算速率(本文提供的案例提升了8倍的计算速度)。文章是基于中科大孟老师的授课内容与笔者目前关注的一个小领域的简单结合。本文主要是分为三大部分,第一部分简单介绍了gRPC协议、第二部分介绍了具体的实现、第三部分是关于本文的总结以及鸣谢。
1 背景及介绍
1.1 什么是gRPC
wiki百科如是介绍:
gRPC 一开始由 google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。
因此在了解gRPC之前,我们需要先了解一下RPC。
RPC(Remote Procedure Call),直译为中文就是远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
详细介绍可以点此参考官方文档
**RPC 架构包含4个核心组件:**客户端(Client)、客户端存根(Client Stub)、服务端(Server)及服务端存根(Server Stub)。
(1)客户端:服务的调用者。
(2)客户端存根:存放服务端的服务列表,将客户端请求打包并通过网络发送到服务端。
(3)服务端:服务提供者。
(4)服务端存根:接收客户端消息并解包,然后调用本地的方法。
整个流程可以概括如下:
(1)客户端以本地调用的方式发起调用,这时调用的其实是客户端存根。
(2)服务端存根在收到调用后,负责将被调用的方法名、参数等打包并编码成特定格式的能进行网络传输的消息体。
(3)客户端存根将消息体通过网络发送给服务端。
(4)服务端存根通过网络接收到消息,按照相应的格式进行拆包、解码,获取方法名和参数。
(5)服务端存根根据方法名和参数进行本地调用,这时调用的是真正的服务提供者。
(6)服务提供者调用本地服务,然后将结果返回给服务端存根。
(7)服务端存根将返回值打包并编码成消息。
(8)服务端存根通过网络将消息发送给客户端。
(9)服务端存根在收到消息后,进行拆包、解码并返回给客户端。
(10)服务端存根得到本次RPC调用的最终结果。
在RPC中一般会用到动态代理、序列化反序列化、NIO网络通信、服务注册和发现等技术。
前文已经简单描述了RPC这种协议,而本文着重探讨的,则是Google基于RPC协议开发出来的框架gRPC,它是基于 ProtoBuf 序列化协议进行开发,支持多种语言(Golang、Python、Java等)。
gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
1.2 gRPC的使用
这部分可以参考中科大孟老师的文档,老师的文档非常详细实用,这里我不再画蛇添足。
孟老师的gRPC_Protobuf 快速编程指南
如果需要获取更为详细的文档信息,可以点此参阅官方文档
2 gRPC在CPU-FPGA上的使用
2.1 背景介绍
作为一个用户,我可能不想去关注具体的功能实现,而只是需要调用某个函数来完成我的需求,所以这个场景就相当合适使用gRPC来完成相关的计算。 比如我想完成一个矩阵的计算(由于个人水平原因,暂时只能实现这个),就可以在FPGA上完成相关的硬件布线资源然后在电脑上机进行相关的函数调用来完成计算。
经过我对gRPC这个框架的进一步了解,我发现它的使用更多是基于分布式框架的应用,刚好我实验室有块ZYNQ7010的FPGA板卡,因此我想尝试一下它的异构调用功能。FPGA是现场可编程门阵列。
现代的我们所使用的电脑的CPU指令是串行执行的,因为现在的计算机体系结构当中的指令流水线(Pipeline)是按照顺序执行的,当然,多核CPU虽然可以并行执行多个进程或线程,但本质上,仍然是基于串行的流水线。
而本文介绍的FPGA基于它特殊的结构,即可以并行执行一些计算,针对这一特性,我设计了一个16 × 16
的脉动阵列块,来用于本文实例当中的两个16 × 16
的矩阵块的计算,来探讨gRPC在异构系统当中通信的作用。
2.2 相关前置知识
FPGA(现场可编程门阵列) 拥有大量的“逻辑门”资源与布线资源,最基本的逻辑门如我们熟悉的与门,或门,非门,异或门。用户可以通过编写硬件verilog
语言经过vivado
或quarts
综合生成对应的硬件电路,传统的CPU
和ASIC
芯片的硬件逻辑在出厂后就已经固定下来了,而FPGA可以多次擦写以实现不同的硬件逻辑。(个人理解,可能有些偏颇)
脉动阵列是一种二维硬件结构,其基本组成单元是一个乘法器和累加寄存器组成的计算单元(MAC),MAC 之间在水平和竖直方向上相互连接形成二维阵列。两个矩阵的输入数据从水平方向和竖直方向上流入,经过 n 个周期后在另一端得到计算结果
可以构造如下的脉动阵列,矩阵A
的每一行从左侧输入,矩阵B
的每一列元素向下输入,比如第一个cycle
之后,元素
a00
与元素·b00
装入处理单元c00
,第二个周期执行乘法然后存储在处理单元当中,元素a00
与元素b00
分别向左和向右流动,这样相比于传统的计算,矩阵元素不用重复加载,且计算可以并行进行,如下图3×3
的矩阵而言,总体只需要6 cycle
就能完成计算。
2.3 核心源代码的实现
2.3.1 硬件布线的实现
block.v
单元设计,这个单元主要负责乘法和加法
module pe(inp_north, inp_west, clk, rst, outp_south, outp_east, result);
input [31:0] inp_north, inp_west;
output reg [31:0] outp_south, outp_east;
input clk, rst;
output reg [63:0] result;
wire [63:0] multi;
always @(posedge rst or posedge clk) begin
if(rst) begin
result <= 0;
outp_east <= 0;
outp_south <= 0;
end
else begin
result <= result + multi;
outp_east <= inp_west;
outp_south <= inp_north;
end
end
assign multi = inp_north*inp_west;
endmodule
16 × 16
的脉动阵列块
避免文章繁琐,这里仅给出核心代码块,部分代码重复较多已经省略。
//16 * 16 个block模块的实例化
//16 * 16 个block模块的实例化
`include "block.v"
module systolic_array(
clk, rst, done);
//from north and west
block P0 (inp_north0, inp_west0, clk, rst, outp_south0, outp_east0, result0);
block P1 (inp_north1, outp_east0, clk, rst, outp_south1, outp_east1, result1);
block P2 (inp_north2, outp_east1, clk, rst, outp_south2, outp_east2, result2);
block P3 (inp_north3, outp_east2, clk, rst, outp_south3, outp_east3, result3);
block P4 (inp_north4, outp_east3, clk, rst, outp_south4, outp_east4, result4);
block P5 (inp_north5, outp_east4, clk, rst, outp_south5, outp_east5, result5);
block P6 (inp_north6, outp_east5, clk, rst, outp_south6, outp_east6, result6);
block P7 (inp_north7, outp_east6, clk, rst, outp_south7, outp_east7, result7);
block P8 (inp_north8, outp_east7, clk, rst, outp_south8, outp_east8, result8);
block P9 (inp_north9, outp_east8, clk, rst, outp_south9, outp_east9, result9);
block P10 (inp_north10, outp_east9, clk, rst, outp_south10, outp_east10, result10);
block P11 (inp_north11, outp_east10, clk, rst, outp_south11, outp_east11, result11);
block P12 (inp_north12, outp_east11, clk, rst, outp_south12, outp_east12, result12);
block P13 (inp_north13, outp_east12, clk, rst, outp_south13, outp_east13, result13);
block P14 (inp_north14, outp_east13, clk, rst, outp_south14, outp_east14, result14);
block P15 (inp_north15, outp_east14, clk, rst, outp_south15, outp_east15, result15);
//from west
block p16 (outp_south0, inp_west16, clk, rst, outp_south16, outp_east16, result16);
block p32 (outp_south16,inp_west32, clk, rst, outp_south32, outp_east32, result32);
block P16 (outp_south0, inp_west16, clk, rst, outp_south16, outp_east16, result16);
block P32 (outp_south16, inp_west32, clk, rst, outp_south32, outp_east32, result32);
block P48 (outp_south32, inp_west48, clk, rst, outp_south48, outp_east48, result48);
block P64 (outp_south48, inp_west64, clk, rst, outp_south64, outp_east64, result64);
block P80 (outp_south64, inp_west80, clk, rst, outp_south80, outp_east80, result80);
block P96 (outp_south80, inp_west96, clk, rst, outp_south96, outp_east96, result96);
block P112 (outp_south96, inp_west112, clk, rst, outp_south112, outp_east112, result112);
block P128 (outp_south112, inp_west128, clk, rst, outp_south128, outp_east128, result128);
block P144 (outp_south128, inp_west144, clk, rst, outp_south144, outp_east144, result144);
block P160 (outp_south144, inp_west160, clk, rst, outp_south160, outp_east160, result160);
block P176 (outp_south160, inp_west176, clk, rst, outp_south176, outp_east176, result176);
block P192 (outp_south176, inp_west192, clk, rst, outp_south192, outp_east192, result192);
block P208 (outp_south192, inp_west208, clk, rst, outp_south208, outp_east208, result208);
block P224 (outp_south208, inp_west224, clk, rst, outp_south224, outp_east224, result224);
block P240 (outp_south224, inp_west240, clk, rst, outp_south240, outp_east240, result240);
// 2 row
block P17 (outp_south1, outp_east16, clk, rst, outp_south17, outp_east17, result17);
block P18 (outp_south2, outp_east17, clk, rst, outp_south18, outp_east18, result18);
block P19 (outp_south3, outp_east18, clk, rst, outp_south19, outp_east19, result19);
block P20 (outp_south4, outp_east19, clk, rst, outp_south20, outp_east20, result20);
block P21 (outp_south5, outp_east20, clk, rst, outp_south21, outp_east21, result21);
block P22 (outp_south6, outp_east21, clk, rst, outp_south22, outp_east22, result22);
block P23 (outp_south7, outp_east22, clk, rst, outp_south23, outp_east23, result23);
block P24 (outp_south8, outp_east23, clk, rst, outp_south24, outp_east24, result24);
block P25 (outp_south9, outp_east24, clk, rst, outp_south25, outp_east25, result25);
block P26 (outp_south10, outp_east25, clk, rst, outp_south26, outp_east26, result26);
block P27 (outp_south11, outp_east26, clk, rst, outp_south27, outp_east27, result27);
block P28 (outp_south12, outp_east27, clk, rst, outp_south28, outp_east28, result28);
block P29 (outp_south13, outp_east28, clk, rst, outp_south29, outp_east29, result29);
block P30 (outp_south14, outp_east29, clk, rst, outp_south30, outp_east30, result30);
block P31 (outp_south15, outp_east30, clk, rst, outp_south31, outp_east31, result31);
// 3 row
// 4 row
// 5 row
// 6 row
// 7 row
// // 8 row
// // 9 row
// //10 row
// //11 row
// //12 row
// //13 row
// //14 row
// //15 row
//16 row
always @(posedge clk or posedge rst) begin
if(rst) begin
done <= 0;
count <= 0;
end
else begin
//256个脉冲之后结束运算
//15 + 16 + 15 = 46个脉冲计算完成
if(count == 50) begin
...
done <= 1;
count <= 0;
end
else begin
done <= 0;
count <= count + 1;
end
end
end
endmodule
上述代码经过仿真布线综合之后生成相应的matmul.bit
文件在本地,之后烧录到ZYNQ板卡上即可运行相关的功能。使用的软件为vivado 2018.3
,仿真软件为ModelsimSE-64
2.3.2 client和server代码实现
首先定义消息格式
matrix_multiplier.proto
syntax = "proto3";
package matrix_multiplier;
service MatrixMultiplier {
rpc MultiplyMatrices (MatrixRequest) returns (MatrixResponse) {}
}
message MatrixRequest {
repeated int32 matrix_a = 1;
repeated int32 matrix_b = 2;
}
message MatrixResponse {
repeated int32 result = 1;
}
之后在命令行输入
$ protoc --python_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_python_plugin` matrix_multiplier.proto
得到matrix_multiplier_pb2
文件和matrix_multiplier_pb2_grpc
文件
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: matrix_multiplier.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17matrix_multiplier.proto\x12\x11matrix_multiplier\"3\n\rMatrixRequest\x12\x10\n\x08matrix_a\x18\x01 \x03(\x05\x12\x10\n\x08matrix_b\x18\x02 \x03(\x05\" \n\x0eMatrixResponse\x12\x0e\n\x06result\x18\x01 \x03(\x05\x32m\n\x10MatrixMultiplier\x12Y\n\x10MultiplyMatrices\x12 .matrix_multiplier.MatrixRequest\x1a!.matrix_multiplier.MatrixResponse\"\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'matrix_multiplier_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_MATRIXREQUEST']._serialized_start=46
_globals['_MATRIXREQUEST']._serialized_end=97
_globals['_MATRIXRESPONSE']._serialized_start=99
_globals['_MATRIXRESPONSE']._serialized_end=131
_globals['_MATRIXMULTIPLIER']._serialized_start=133
_globals['_MATRIXMULTIPLIER']._serialized_end=242
# @@protoc_insertion_point(module_scope)
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import matrix_multiplier_pb2 as matrix__multiplier__pb2
class MatrixMultiplierStub(object):
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.MultiplyMatrices = channel.unary_unary(
'/matrix_multiplier.MatrixMultiplier/MultiplyMatrices',
request_serializer=matrix__multiplier__pb2.MatrixRequest.SerializeToString,
response_deserializer=matrix__multiplier__pb2.MatrixResponse.FromString,
)
class MatrixMultiplierServicer(object):
"""Missing associated documentation comment in .proto file."""
def MultiplyMatrices(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_MatrixMultiplierServicer_to_server(servicer, server):
rpc_method_handlers = {
'MultiplyMatrices': grpc.unary_unary_rpc_method_handler(
servicer.MultiplyMatrices,
request_deserializer=matrix__multiplier__pb2.MatrixRequest.FromString,
response_serializer=matrix__multiplier__pb2.MatrixResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'matrix_multiplier.MatrixMultiplier', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class MatrixMultiplier(object):
"""Missing associated documentation comment in .proto file."""
@staticmethod
def MultiplyMatrices(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/matrix_multiplier.MatrixMultiplier/MultiplyMatrices',
matrix__multiplier__pb2.MatrixRequest.SerializeToString,
matrix__multiplier__pb2.MatrixResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
matrix_multiplier_client.py
import grpc
import matrix_multiplier_pb2
import matrix_multiplier_pb2_grpc
from pynq import Overlay
def run():
# 连接到 gRPC 服务器
channel = grpc.insecure_channel('localhost:99999')
stub = matrix_multiplier_pb2_grpc.MatrixMultiplierStub(channel)
# 构造矩阵请求
matrix_a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
matrix_b = [9, 8, 7, 6, 5, 4, 3, 2, 1]
request = matrix_multiplier_pb2.MatrixRequest(
matrix_a=matrix_a,
matrix_b=matrix_b
)
# 发送请求并获取结果
response = stub.MultiplyMatrices(request)
print("Result Matrix:", response.result)
if __name__ == '__main__':
run()
matrix_multiplier_server.py
gRPC服务器
import grpc
from concurrent import futures
import matrix_multiplier_pb2
import matrix_multiplier_pb2_grpc
from pynq import Overlay
import time
class MatrixMultiplierServicer(matrix_multiplier_pb2_grpc.MatrixMultiplierServicer):
def __init__(self):
super().__init__()
self.overlay = Overlay("matmul_bitstream.bit")
self.matrix_multiplier_ip = self.overlay.matrix_multiplier_0
def MultiplyMatrices(self, request, context):
# 从 gRPC 请求中获取矩阵数据
matrix_a = request.matrix_a.values
matrix_b = request.matrix_b.values
# 将数据传递给硬件矩阵乘法器 IP 核
self.matrix_multiplier_ip.write(0x10, matrix_a)
self.matrix_multiplier_ip.write(0x18, matrix_b)
# 启动硬件计算
self.matrix_multiplier_ip.write(0x00, 0x01)
# 等待硬件计算完成
while not self.matrix_multiplier_ip.read(0x00) & 0x4:
pass
# 获取性能计数器值
cycle_count = self.matrix_multiplier_ip.read(0x4)
# 打印 FPGA 加速模块的执行周期数
print(f"FPGA Accelerator Cycle Count: {cycle_count}")
# 从硬件读取结果
result = self.matrix_multiplier_ip.read(0x20, 256) # Assuming a 16x16 matrix result
return matrix_multiplier_pb2.MatrixResponse(result=result)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
matrix_multiplier_pb2_grpc.add_MatrixMultiplierServicer_to_server(
MatrixMultiplierServicer(), server)
port = 9999
server_address = f'[::]:{port}'
print(f'Starting gRPC server on {server_address}')
# server.add_insecure_port('localhost:99999')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
serve()
2.4 结果与分析
这里以16 × 16
的随机数组相乘并返回结果数组得到如下结果
为了做一组对比实验,我简单编写了一个python编写计算程序。
import numpy as np
import timeit
# 定义表达式
setup_code = """
import numpy as np
matrix_a = np.random.randint(1, 10, size=(16, 16))
matrix_b = np.random.randint(1, 10, size=(16, 16))
"""
execution_code = """
matrix_result = np.dot(matrix_a, matrix_b)
"""
# 使用timeit运行代码并测量时间
time_taken = timeit.timeit(execution_code, setup=setup_code, number=100)
# 输出平均执行时间
print(f"Average execution time: {time_taken / 100 * 1000000000} nanosecond")
可以看到CPU执行这个矩阵运算需要3085 ns
,前面的使用FPGA进行相同的运算大概需要46 * 10 = 460 ns
,速度提升了8
倍左右
3 总结
本文使用gRPC框架简单实现了CPU-FPGA的异构系统关于矩阵乘法的运算,通过一个小的benchmark
我们可以很直观地看到让具有特性的硬件去完成相关的运算,可以高效提升我们运算速率(本文提供的案例提升了8倍的计算速度)。
虽然该次小实验从我的突发奇想到具体调研花了不少时间,但是还有许多可以进一步提升的地方:
- 将这个框架手搓为硬件描述语言
verilog
语言来实现,这是自己一开始的构想,但由于各种条件限制,我发现这是一个非常浩大的工程,所以只能止步于前文所描述的地方,笔者整个研究生期间或可围绕这个开展一些工作,这部分可以参考一位大佬的作品,他使用verilog
手搓了整个TCP/IP协议栈,令人叹为观止,点此前往大佬博客。 - 这部分内容目前暂时尚未看到完整的代码仓库,当然笔者找到了一篇相关的论文,遗憾的是作者并没有对其进行代码开源。有兴趣的读者可自行了解他们的内容。Calling hardware procedures in a reconfigurable accelerator using RPC-FPGA
- 笔者深信,随着数据迎来进一步的爆发性增长,程序并行性在未来将变得愈加重要。异构系统在各个领域必将迸发出巨大的生命力,而高性能通信框架和卓越的异构系统,将成为未来片上系统中不可或缺的生命支柱。。
特别鸣谢
文章的最后,深深感谢中科大的孟老师。 孟老师非常善于引导学生的发散思维,让我们进行一些挑战,比如本文的背景就是笔者上课后突发奇想而开始着手调研、实现的。孟老师不仅专业知识深厚,而且涉猎很广,通过这次的网络程序设计课程,我深入到网络协议底层,学会了许多网络协议的本质。特别地,孟老师与其他授课老师不同的是,他的课上往往在潜意识里和学生强调要将个人命运与国家命运深深融合,鼓励我们向更为困难、更有挑战性的事情发起冲锋,他的言辞教诲之中充满着对知识的热爱和对祖国的深情厚谊,为我们树立了崇高的奉献精神典范,在未来的深入学习之中,更要秉持孟老师的敦敦教诲,为祖国做出个人绵薄的贡献。
some link
- https://developer.aliyun.com/article/1074501
- https://doc.oschina.net/grpc?t=58008
- https://core.ac.uk/download/pdf/153399229.pdf
- https://indico.cern.ch/event/942656/contributions/3960947/attachments/2100003/3530357/irishep_aas_09sep20.pdf
- https://ieeexplore.ieee.org/document/8280158
- https://www.zhihu.com/question/30027669/answer/1694567359