天天看點

五分鐘搭建BERT服務,實作1000+QPS​,這個Service-Streamer做到了

作者 | 劉欣

簡介:劉欣,Meteorix,畢業于華中科技大學,前網易遊戲技術總監,現任香侬科技算法架構負責人。之前專注遊戲引擎工具架構和自動化領域,2018年在GDC和GoogleIO開源Airtest自動化架構,廣泛應用于Unity3d/Cocos2dx遊戲和網易、暴雪、SE等公司。目前負責香侬NLP領域工程化、算法平台架構。

深度學習模型在訓練和測試時,通常使用小批量(mini-batch)的方式将樣本組裝在一起,這樣能充分利用GPU的并行計算特性,加快運算速度。

但在将使用了深度學習模型的服務部署上線時,由于使用者請求通常是離散和單次的,若采取傳統的循環伺服器或多線程伺服器,在短時間内有大量請求時,會造成GPU計算資源閑置,使用者等待時間線性變長。

基于此,我們開發了service-streamer,它是一個中間件,将服務請求排隊組成一個完整的batch,再送進GPU運算。這樣可以犧牲最小的時延(預設最大0.1s),提升整體性能,極大優化GPU使用率。

功能特色

  • 簡單易用: 隻需添加兩三行代碼即可讓模型服務提速上數十倍。
  • 處理高速: 高QPS、低延遲,專門針對速度做了優化,見基準測試。
  • 擴充性好: 可輕松擴充到多GPU場景,處理大量請求,見分布式。
  • 适用性強: 中間件,适用于所有深度學習架構和web架構。

安裝步驟

可通過pip安裝,要求Python>=3.5:

pip install service_streamer

五分鐘搭建BERT服務

為了示範API使用方法,service-streamer提供了一個完整的教程和示例代碼。如何在五分鐘搭建起基于BERT模型的完形填空服務,每秒處理1000+請求。

GitHub連結:https://github.com/ShannonAI/service-streamer

1、首先我們定義一個完型填空模型(bert_model.py),其predict方法接受批量的句子,并給出每個句子中[MASK]位置的預測結果。

class TextInfillingModel (object);

...

batch=["twinkletwinkle [MASK] star",

"Happy birthday to [MASK]",

'the answer to life, the [MASK], andeverything']

model=TextaInfillingModel()

outputs=model.predict(batch)

print(outputs)

#['little', 'you', 'universe' ]

2、然後使用Flask将模型封裝成web服務flask_example.py。這時候你的web服務每秒鐘隻能完成12句請求。

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封裝你的模型函數,三行代碼使BERT服務的預測速度達到每秒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)

returnisonify(outputs)

app.run(port=5005,debug=False)

4、最後,我們利用Streamer封裝模型,啟動多個GPU worker,充分利用多卡性能實作每秒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(ManageBertModel, batch_size=64, max_latency=0.1,

worker_num = 8, cuda_devices=(0,1,2,3))

app.run(port=5005,debug=False)

運作flask_multigpu_example.py這樣即可啟動8個GPUworker,平均配置設定在4張卡上。

更多指南

除了上面的5分鐘教程,service-streamer還提供了:

  • 分布式API使用方法,可以配合gunicorn實作web server和gpuworker的分布式;
  • 異步FutureAPI,在本地高頻小batch調用的情形下如何利用service-streamer加速;
  • 性能Benchmark,利用wrk進行單卡和多卡的性能測試資料。

API介紹

快速入門

通常深度學習的inference按batch輸入會比較快。

outputs= model.predict(batch_inputs)

用service_streamer中間件封裝predict函數,将request排隊成一個完整的batch,再送進GPU。犧牲一定的時延(預設最大0.1s),提升整體性能,極大提高GPU使用率。

from service_streamer import ThreadedStreamer

# 用Streamer封裝batch_predict函數

streamer= ThreadedStreamer(model.predict, batch_size=64, max_latency=0.1)

# 用Streamer封裝batch_predict函數

outputs= streamer.predict(batch_inouts)

然後你的web server需要開啟多線程(或協程)即可。

短短幾行代碼,通常可以實作數十(batch_size/batch_per_request)倍的加速。

分布式GPU worker

上面的例子是在web server程序中,開啟子線程作為GPUworker進行 batch predict,用線程間隊列進行通信和排隊。

實際項目中web server的性能(QPS)遠高于GPU模型的性能,是以我們支援一個web server搭配多個GPUworker程序。

import multiprocessing;

multiprocessing.set_start_method("spawn",force=True)

from service_streamer import Streamer

#spawn出4個gpu worker程序

streamer= Streamer(model.predict, 64, 0.1,worker_num=4)

outputs= streamer.redict(batch)

Streamer預設采用spawn子程序運作gpuworker,利用程序間隊列進行通信和排隊,将大量的請求配置設定到多個worker中處理,再将模型batch predict的結果傳回到對應的web server,并且傳回到對應的http response。

五分鐘搭建BERT服務,實作1000+QPS​,這個Service-Streamer做到了

上面這種方式定義簡單,但是主程序初始化模型,多占了一份顯存,并且模型隻能運作在同一塊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個gpu worker程序,平均分數在0/1/2/3号GPU上

streamer=Streamer(ManagedBertModel, 64, 0.1,worker_num=4,cuda_devices=(0,1,2,3))

outputs = streamer.predict(batch)

分布式web server

有時候,你的web server中需要進行一些CPU密集型計算,比如圖像、文本預處理,再配置設定到GPU worker進入模型。CPU資源往往會成為性能瓶頸,于是我們也提供了多web server搭配(單個或多個)GPU worker的模式。

使用跟任意RedisStreamer指定所有web server 和GPU worke的模式。

# 預設參數可以省略,使用localhost:6379

streamer = RedisStreamer

(redis_broker="172.22.22.22:6379")

然後跟任意python web server的部署一樣,用gunicorn或uwsgi實作反向代理和負載均衡。

cd example

gunicorn -c redis_streamer_gunicorn.py flask_example:app

這樣每個請求會負載均衡到每個web server中進行CPU預處理,然後均勻的分布到GPU worke中進行模型predict。

Future API

如果你使用過任意concurrent庫,應該對future不陌生。當你的使用場景不是web service,又想使用service_streamer進行排隊或者分布式GPU計算,可以直接使用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對象,再等待異步傳回

for future in xs:

outputs = future.result()

print(outputs)

基準測試

如何做基準測試

我們使用wrk來使做基準測試。

環境

  • GPU : Titan Xp
  • cuda : 9.0
  • python : 1.1

單個GPU程序

# startflask 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.luahttp://127.0.0.1:5005/naive

#benchmark stream api with service_streamer

./wrk -t 4 -c 128 -d 20s --timeout=10s -s scripts/streamer.luahttp://127.0.0.1:5005/naive

五分鐘搭建BERT服務,實作1000+QPS​,這個Service-Streamer做到了

多個GPU程序

這裡對比單web server程序的情況下,多GPU worker的性能,驗證通過和負載均衡機制的性能損耗。Flask多線程server已經成為性能瓶頸,故采用gevent server。

五分鐘搭建BERT服務,實作1000+QPS​,這個Service-Streamer做到了

利用Future API使用多個GPU

為了規避web server的性能瓶頸,我們使用底層Future API本地測試多GPU worker的benchmark。

可以看出service_streamer的性能跟GPUworker數量及乎成線性關系,其中程序間通信的效率略高于redis通信。

(*本文為 AI科技大學營轉載文章,轉載請聯系原作者)