0x00 前言
近期興趣使然的技術調研越發的少了(TTS算一個),主要的都是爲了項目和任務去研究的東西。目前的情況是爲了節約顯存,對一個較大的模型而言,比起使用4個worker來重複的佔用顯存,不如只佔用一份顯存,但是開啓服務流式或觸發式地處理不同項目的需求。
於是 @caoyixuan93 學長向我推薦了GRPC,經過 @hongfeng 和 @phchang 的幫助,終於得以成功實現了一個小的自定義demo,後續再花些時間把模型裝載上去。
0x01 GRPC介紹
GRPC: A high performance, open source, general-purpose RPC framework
簡單的來說,就是一個開源的“服務端-客戶端”框架,你可以把你的服務(例如模型的預測函數)掛載起來,隨時接受到通過端口發送來的輸入數據,計算後將輸出返回回去。當有多個訪問時,以隊列或者流的形式逐個處理。
0x02 環境配置
我們來參照 Python Quick Start ,對於一個簡單的grpc服務,逐步搭建起來依次需要哪些東西,從而來對應的看看需要配置的環境吧:
Python >= 3.4
grpc有很多版本,這裏我選擇使用便捷的pythongrpcio>=1.28.1
既然要用grpc那自然要裝上grpc了,這裏的grpcio
是指grpc的python版本,此外還有其他各種語言的版本就沒有多做研究啦,直接pip install grpcio
即可grpcio-tools
這個工具的作用是通過讀取.proto
格式的配置文件,產生兩段python代碼供直接調用,直接pip install grpcio-tools
即可protobuf==3.6.0
上一步的pip install
會附贈安裝一個3.11.x
的,結果版本太高了反而容易出錯,這裏我們選用穩定的3.6.0
版本了,用途是讀取上面提到的.proto
格式的文件,進行服務的變量名配置
0x03 源碼及分析
個人可能腦袋瓜比較笨,看 Python Quick Start 裏的例子是硬生生沒明白,所以比起他的helloworld,不如自己自定義一個demo來嘗試跑通,這用來學習一種新框架是一個不錯的方法。
配置文件
https://github.com/okcd00/CDAlter/tree/master/CDMemory/protos
首先是令人勸退的 protobuf 語言,啊我沒學過怎麼辦,啊看起來頭好疼。
然後看着看着,哦,就這呀?還好還好,又不用寫邏輯,就是配置嘛,那我會,yml我也不會呀但我照葫蘆畫瓢寫配置還是能做到的嘛。
// 意思是我們用了proto3的版本,不用管這行,大概python2會變成proto2吧,我瞎猜的
syntax = "proto3";
// 我們自定義的這個配置文件叫啥,會用在產生代碼的時候當名字用
package keyvaluestore;
// service裏就是預先定義一下你這個服務有哪些函數可以調用
// 這裏只需要定義不需要實現任何功能,功能都是在python裏實現的
service KeyValueStore {
// 函數格式: rpc 函數名 (輸入變量) returns (輸出變量) {}
// 變量都要在下面用 message 來定義了才能用哦
rpc ask (Key) returns (Response) {}
rpc remember (Item) returns (Response) {}
}
// 變量名:Key
// 有一個成員變量,爲詞典裏的鍵
message Key {
string key = 1;
}
// 變量名:Item
// 有兩個成員變量,爲詞典裏的鍵和值
message Item {
string key = 1;
string value = 2;
// 這個1和2就當作是標識符就好,有幾個變量就要寫到幾,這樣服務器才能數的清楚不會搞錯
}
// 變量名:Response
// 有一個成員變量,爲詞典裏的值
message Response {
string value = 1;
}
這個proto
文件設置好了之後,通過命令行(cmd,命令提示符,git bash都可以)來調用tool自動生成兩個python文件,調用方法爲,先進入proto所在的文件夾(例如 cd /i/Github/CDAlter/CDMemory/protos
),然後執行如下命令(這裏我的proto文件叫作keyvaluestore.proto
):
python -m grpc_tools.protoc \
-I ./ \
--python_out=. \
--grpc_python_out=. \
keyvaluestore.proto
裏面的 \
是爲了格式好看點,偷懶的話在提供個可以直接輸入一整行的:
python -m grpc_tools.protoc -I ./ --python_out=. --grpc_python_out=. keyvaluestore.proto
因爲我的配置文件叫keyvaluestore.proto
,所以在當前目錄下生成了keyvaluestore_pb2.py
和keyvaluestore_pb2_grpc.py
兩個文件。
服務端與客戶端
https://github.com/okcd00/CDAlter/tree/master/CDNerve
服務端
# -*- coding: gbk -*-
# ==========================================================================
# Copyright (C) since 2020 All rights reserved.
#
# filename : grpc_server.py
# author : chendian / [email protected]
# date : 2020-04-16
# desc : server in grpc service
# ==========================================================================
import sys
import time
import grpc
from concurrent import futures
from multiprocessing import Pool
from collections import OrderedDict
from grpc._cython.cygrpc import CompressionAlgorithm, CompressionLevel
"""
# generate it at first
python -m grpc_tools.protoc \
-I ./ \
--python_out=. \
--grpc_python_out=. \
keyvaluestore.proto
# then you can get keyvaluestore_pb2_grpc and keyvaluestore_pb2
"""
# 這裏我的protos不在當前目錄下,所以加了個pythonpath
sys.path.append('../CDMemory/protos/')
from CDMemory.protos import keyvaluestore_pb2_grpc, keyvaluestore_pb2
class KVServicer(keyvaluestore_pb2_grpc.KeyValueStoreServicer):
def __init__(self):
# 這裏可以把模型什麼的都放進來,比如 self.model = AlbertModel()
self.records = OrderedDict() # 這裏用一個字典來作爲Demo服務的載體
# 之前在 service 的部分預先定義的函數都要在這裏重載,而且變量名的個數要一致,
# 不然會報錯:"Exception iterating requests!"
# 我這裏統一用了重載的函數名,也可以寫成 `def ask(self, Key, context):`
def ask(self, request, context):
# 對應 rpc ask (Key) returns (Response) {}
# 這個 request 就是 `ask` 後面那個 `Key`
_value = self.records.get(request.key, "Empty")
# 這個`keyvaluestore_pb2.Response`就是 `returns` 後面的那個 `Response`
return keyvaluestore_pb2.Response(value=_value)
# 我這裏統一用了重載的函數名,也可以寫成 `def remember(self, Item, context):`
def remember(self, request, context):
# 對應 rpc remember (Item) returns (Response) {}
# 這個 request 就是 `remember ` 後面那個 `Item`
key, value = request.key, request.value # 就是我們在Item裏定義的key/value
self.records.update({key: value}) # 這個grpc的demo效果就是字典的寫和查
return keyvaluestore_pb2.Response(
value="Remembered: the value for {} is {}".format(key, value))
def serve(n_worker=4, port=20416):
max_receive_message_length = 512
# 初始化 Servicer 實例
service = KVServicer()
# 多線程池,設定線程數量
service.process_pool = Pool(processes=n_worker)
# 建立 server 實例
server = grpc.server(futures.ThreadPoolExecutor(max_workers=n_worker),
options=[ # 這裏的options可以省去不要,我這裏加的設置是限制最大長度和壓縮
('grpc.max_receive_message_length', max_receive_message_length),
('grpc.default_compression_algorithm', CompressionAlgorithm.gzip),
('grpc.grpc.default_compression_level', CompressionLevel.high)
])
# 用自動生成的 keyvaluestore_pb2_grpc 來添加 Servicer 到服務中
keyvaluestore_pb2_grpc.add_KeyValueStoreServicer_to_server(service, server)
# 設置端口號(內網外網均可訪問)
server.add_insecure_port('[::]:{}'.format(port))
# 服務啓動
server.start()
# server.wait_for_termination()
return server
if __name__ == "__main__":
print("starting server...")
server_agent = serve()
print("server started.")
while True:
time.sleep(10000)
print("[ALIVE] {}".format(time.ctime()))
客戶端
# -*- coding: gbk -*-
# ==========================================================================
# Copyright (C) since 2020 All rights reserved.
#
# filename : grpc_client.py
# author : chendian / [email protected]
# date : 2020-04-16
# desc : client in grpc service
# ==========================================================================
import os
import sys
if os.environ.get('https_proxy'):
del os.environ['https_proxy']
if os.environ.get('http_proxy'):
del os.environ['http_proxy']
import grpc
"""
# generate it at first
python -m grpc_tools.protoc \
-I ./ \
--python_out=. \
--grpc_python_out=. \
keyvaluestore.proto
# then you can get keyvaluestore_pb2_grpc and keyvaluestore_pb2
"""
# 這裏我的protos不在當前目錄下,所以加了個pythonpath
sys.path.append('../CDMemory/protos/')
from CDMemory.protos import keyvaluestore_pb2_grpc, keyvaluestore_pb2
if __name__ == '__main__':
# 連接上對應端口的grpc服務
with grpc.insecure_channel('localhost:20416') as channel:
# 初始化一個實例作爲客戶端
stub = keyvaluestore_pb2_grpc.KeyValueStoreStub(channel)
# 調用我們定義的remember函數,效果是寫入 key=name, value=cd
response = stub.remember(keyvaluestore_pb2.Item(key='name', value='cd'))
print(response) # value: "Remembered: the value for name is cd"
# 調用我們定義的ask函數,效果是查詢 key=name 對應的 value
response = stub.ask(keyvaluestore_pb2.Key(key='name'))
print(response) # value: "cd"