作者丨劉欣
單位丨香儂科技算法架構(gòu)負(fù)責(zé)人
研究方向丨NLP工程化、算法平臺(tái)架構(gòu)
深度學(xué)習(xí)模型在訓(xùn)練和測試時(shí),通常使用小批量(mini-batch)的方式將樣本組裝在一起,這樣能充分利用 GPU 的并行計(jì)算特性,加快運(yùn)算速度。
但在將使用了深度學(xué)習(xí)模型的服務(wù)部署上線時(shí),由于用戶請(qǐng)求通常是離散和單次的,若采取傳統(tǒng)的循環(huán)服務(wù)器或多線程服務(wù)器,在短時(shí)間內(nèi)有大量請(qǐng)求時(shí),會(huì)造成 GPU 計(jì)算資源閑置,用戶等待時(shí)間線性變長。
基于此,我們開發(fā)了 service-streamer,它是一個(gè)中間件,將服務(wù)請(qǐng)求排隊(duì)組成一個(gè)完整的 batch,再送進(jìn) GPU 運(yùn)算。這樣可以犧牲最小的時(shí)延(默認(rèn)最大 0.1s),提升整體性能,極大優(yōu)化 GPU 利用率。
Github開源鏈接:
https://github.com/ShannonAI/service-streamer
功能特色
簡單易用:只需添加兩三行代碼即可讓模型服務(wù)提速上數(shù)十倍。
處理高速:高 QPS、低延遲,專門針對(duì)速度做了優(yōu)化,見基準(zhǔn)測試。
擴(kuò)展性好:可輕松擴(kuò)展到多 GPU 場景,處理大量請(qǐng)求,見分布式。
適用性強(qiáng):中間件,適用于所有深度學(xué)習(xí)框架和 web 框架。
安裝步驟
可通過 pip 安裝,要求 Python>=3.5:pip installservice_streamer
五分鐘搭建BERT服務(wù)
為了演示 API 使用方法,service-streamer 提供了一個(gè)完整的教程和示例代碼。如何在五分鐘搭建起基于 BERT 模型的完形填空服務(wù),每秒處理 1000+ 請(qǐng)求。點(diǎn)擊文章左下角“閱讀原文”查看完整代碼。1. 首先我們定義一個(gè)完型填空模型(bert_model.py),其 predict 方法接受批量的句子,并給出每個(gè)句子中 [MASK] 位置的預(yù)測結(jié)果。class TextInfillingModel(object);
...
batch=['twinkle twinkle [MASK] star',
'Happy birthday to [MASK]',
'the answer to life, the [MASK], and everything']
model=TextaInfillingModel()
outputs=model.predict(batch)
print(outputs)
#['little', 'you', 'universe' ]
2. 然后使用 Flask 將模型封裝成 web 服務(wù) flask_example.py。這時(shí)候你的 web 服務(wù)每秒鐘只能完成 12 句請(qǐng)求。model=TextInfillingModel()
@app.route('/naive', methods=['POST'])
def naive_predict( ):
inputs = request.form.getlist('s')
outputs = model.predict(inputs)
return jsonify(outputs)
app.run(port=5005)
3. 下面我們通過 service_streamer 封裝你的模型函數(shù),三行代碼使 BERT 服務(wù)的預(yù)測速度達(dá)到每秒 200+ 句(16 倍 QPS)。from service_streamer import ThreadStreamer streamer= ThreadedStreamer (model.predict,batch_size=64, max_latency=0.1)
@app.route('/stream', methods=['POST'])
def stream_predict( ):
inputs = request.form.getlist('s')
outputs = streamer.predict(inputs)
return isonify(outputs)
app.run(port=5005, debug=False)
4. 最后,我們利用 Streamer 封裝模型,啟動(dòng)多個(gè) GPU worker,充分利用多卡性能實(shí)現(xiàn)每秒 1000+ 句(80 倍 QPS)。import multiprocessing
from service_streamer import ManagedModel, Streamer
multiprocessing.set_start_method('spawn', force=True)
class ManagedBertModel(ManagedModel):
def init_model(self):
self.model = TextInfillingModel( )
def predict(self, batch):
return self.model.predict(batch)
streamer =Streamer(ManagedBertModel, batch_size=64, max_latency=0.1,
worker_num = 8, cuda_devices=(0,1,2,3))
app.run(port=5005, debug=False)
運(yùn)行 flask_multigpu_example.py 這樣即可啟動(dòng) 8 個(gè) GPU worker,平均分配在 4 張卡上。更多指南
除了上面的 5 分鐘教程,service-streamer 還提供了:
分布式 API 使用方法,可以配合 gunicorn 實(shí)現(xiàn) web server 和 gpu worker 的分布式;
異步 Future API,在本地高頻小 batch 調(diào)用的情形下如何利用 service-streamer 加速;
性能 Benchmark,利用 wrk 進(jìn)行單卡和多卡的性能測試數(shù)據(jù)。
API介紹
通常深度學(xué)習(xí)的 inference 按 batch 輸入會(huì)比較快。outputs = model.predict(batch_inputs)
用 service_streamer 中間件封裝 predict 函數(shù),將 request 排隊(duì)成一個(gè)完整的 batch,再送進(jìn) GPU。犧牲一定的時(shí)延(默認(rèn)最大 0.1s),提升整體性能,極大提高 GPU 利用率。from service_streamer import ThreadedStreamer
# 用Streamer封裝batch_predict函數(shù)
streamer = ThreadedStreamer(model.predict, batch_size=64, max_latency=0.1)
# 用Streamer異步調(diào)用predict函數(shù)
outputs = streamer.predict(batch_inouts)
然后你的 web server 需要開啟多線程(或協(xié)程)即可。 短短幾行代碼,通??梢詫?shí)現(xiàn)數(shù)十(batch_size/batch_per_request)倍的加速。上面的例子是在 web server 進(jìn)程中,開啟子線程作為 GPU worker 進(jìn)行 batch predict,用線程間隊(duì)列進(jìn)行通信和排隊(duì)。 實(shí)際項(xiàng)目中 web server 的性能(QPS)遠(yuǎn)高于 GPU 模型的性能,所以我們支持一個(gè) web server 搭配多個(gè) GPU worker 進(jìn)程。import multiprocessing;
multiprocessing.set_start_method('spawn', force=True)
from service_streamer import Streamer
# spawn出4個(gè)gpu worker進(jìn)程
streamer = Streamer(model.predict, 64, 0.1, worker_num=4)
outputs = streamer.redict(batch)
Streamer 默認(rèn)采用 spawn 子進(jìn)程運(yùn)行 gpu worker,利用進(jìn)程間隊(duì)列進(jìn)行通信和排隊(duì),將大量的請(qǐng)求分配到多個(gè) worker 中處理,再將模型 batch predict 的結(jié)果傳回到對(duì)應(yīng)的 web server,并且返回到對(duì)應(yīng)的 http response。
上面這種方式定義簡單,但是主進(jìn)程初始化模型,多占了一份顯存,并且模型只能運(yùn)行在同一塊 GPU 上,所以我們提供了 ManageModel 類,方便模型 lazy 初始化和遷移,以支持多 GPU。from service_streamer import ManagedModel
class ManagedBertModel(ManagedModel):
def init_model(self):
self.model = Model( )
def predict(self, batch):
return self.model.predict(batch)
# spawn出4個(gè)gpu worker進(jìn)程,平均分?jǐn)?shù)在0/1/2/3號(hào)GPU上
streamer = Streamer(ManagedBertModel, 64, 0.1, worker_num=4,cuda_devices=(0,1,2,3))
outputs = streamer.predict(batch)
有時(shí)候,你的 web server 中需要進(jìn)行一些 CPU 密集型計(jì)算,比如圖像、文本預(yù)處理,再分配到 GPU worker 進(jìn)入模型。CPU 資源往往會(huì)成為性能瓶頸,于是我們也提供了多 web server 搭配(單個(gè)或多個(gè))GPU worker 的模式。使用 RedisStreamer 指定所有 web server 和 GPU worker 共用的 redis broker 地址。# 默認(rèn)參數(shù)可以省略,使用localhost:6379
streamer = RedisStreamer
(redis_broker='172.22.22.22:6379')
然后跟任意 python web server 的部署一樣,用 gunicorn 或 uwsgi 實(shí)現(xiàn)反向代理和負(fù)載均衡。cd example
gunicorn -c redis_streamer_gunicorn.py flask_example:app
這樣每個(gè)請(qǐng)求會(huì)負(fù)載均衡到每個(gè) web server 中進(jìn)行 CPU 預(yù)處理,然后均勻的分布到 GPU worke 中進(jìn)行模型 predict。Future API
如果你使用過任意 concurrent 庫,應(yīng)該對(duì) future 不陌生。當(dāng)你的使用場景不是 web service,又想使用 service_streamer 進(jìn)行排隊(duì)或者分布式 GPU 計(jì)算,可以直接使用 Future API。from service_streamer import ThreadedStreamer
streamer = ThreadedStreamer(model.predict, 64, 0.1)
xs ={}
for i in range(200):
future = streamer.submit(['Happy birthday to [MASK]',
'Today is my lucky [MASK]'])
xs.append(future)
# 先拿到所有future對(duì)象,再等待異步返回
for future in xs:
outputs = future.result()
print(outputs)
基準(zhǔn)測試
GPU : Titan Xp
cuda : 9.0
pytorch : 1.1
# start flask threaded server
python example/flask_example.py
# benchmark naive api without service_streamer
./wrk -t 4 -c 128 -d 20s --timeout=10s -s scripts/streamer.lua http://127.0.0.1:5005/naive
# benchmark stream api with service_streamer
./wrk -t 4 -c 128 -d 20s --timeout=10s -s scripts/streamer.lua http://127.0.0.1:5005/naive

這里對(duì)比單 web server 進(jìn)程的情況下,多 GPU worker 的性能,驗(yàn)證通過和負(fù)載均衡機(jī)制的性能損耗。Flask 多線程 server 已經(jīng)成為性能瓶頸,故采用 gevent server。

為了規(guī)避 web server 的性能瓶頸,我們使用底層 Future API 本地測試多 GPU worker 的 benchmark。
可以看出 service_streamer 的性能跟 GPUworker 數(shù)量及乎成線性關(guān)系,其中進(jìn)程間通信的效率略高于 redis 通信。