开源项目 | 五分钟搭建BERT服务,实现1000+QPS
作者丨劉欣
單位丨香儂科技算法架構(gòu)負(fù)責(zé)人
研究方向丨NLP工程化、算法平臺架構(gòu)
深度學(xué)習(xí)模型在訓(xùn)練和測試時(shí),通常使用小批量(mini-batch)的方式將樣本組裝在一起,這樣能充分利用 GPU 的并行計(jì)算特性,加快運(yùn)算速度。?
但在將使用了深度學(xué)習(xí)模型的服務(wù)部署上線時(shí),由于用戶請求通常是離散和單次的,若采取傳統(tǒng)的循環(huán)服務(wù)器或多線程服務(wù)器,在短時(shí)間內(nèi)有大量請求時(shí),會造成 GPU 計(jì)算資源閑置,用戶等待時(shí)間線性變長。?
基于此,我們開發(fā)了 service-streamer,它是一個(gè)中間件,將服務(wù)請求排隊(duì)組成一個(gè)完整的 batch,再送進(jìn) GPU 運(yùn)算。這樣可以犧牲最小的時(shí)延(默認(rèn)最大 0.1s),提升整體性能,極大優(yōu)化 GPU 利用率。
Github開源鏈接:
https://github.com/ShannonAI/service-streamer
功能特色
安裝步驟
可通過 pip 安裝,要求 Python>=3.5:
五分鐘搭建BERT服務(wù)
1. 首先我們定義一個(gè)完型填空模型(bert_model.py),其 predict 方法接受批量的句子,并給出每個(gè)句子中 [MASK] 位置的預(yù)測結(jié)果。
2. 然后使用 Flask 將模型封裝成 web 服務(wù) flask_example.py。這時(shí)候你的 web 服務(wù)每秒鐘只能完成 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)
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)import?ThreadStreamer?streamer=?ThreadedStreamer?(model.predict,batch_size=64,?max_latency=0.1)
def?stream_predict(?):
?????inputs?=?request.form.getlist("s")
????outputs?=?streamer.predict(inputs)
????return?isonify(outputs)
app.run(port=5005,?debug=False)
import?multiprocessingfrom?service_streamer?import?ManagedModel,?Streamermultiprocessing.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)
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)
更多指南
除了上面的 5 分鐘教程,service-streamer 還提供了:?
API介紹
快速入門
通常深度學(xué)習(xí)的 inference 按 batch 輸入會比較快。
outputs?=?model.predict(batch_inputs)
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)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)倍的加速。
分布式GPU worker?
上面的例子是在 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)
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ì),將大量的請求分配到多個(gè) worker 中處理,再將模型 batch predict 的結(jié)果傳回到對應(yīng)的 web server,并且返回到對應(yīng)的 http response。
上面這種方式定義簡單,但是主進(jìn)程初始化模型,多占了一份顯存,并且模型只能運(yùn)行在同一塊 GPU 上,所以我們提供了 ManageModel 類,方便模型 lazy 初始化和遷移,以支持多 GPU。
from?service_streamer?import?ManagedModelclass?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號GPU上streamer?=?Streamer(ManagedBertModel,?64,?0.1,?worker_num=4,cuda_devices=(0,1,2,3))outputs?=?streamer.predict(batch)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號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 資源往往會成為性能瓶頸,于是我們也提供了多 web server 搭配(單個(gè)或多個(gè))GPU worker 的模式。
使用 RedisStreamer 指定所有 web server 和 GPU worker 共用的 redis broker 地址。
#?默認(rèn)參數(shù)可以省略,使用localhost:6379streamer?=?RedisStreamer(redis_broker="172.22.22.22:6379")
streamer?=?RedisStreamer
(redis_broker="172.22.22.22:6379")
cd?examplegunicorn?-c?redis_streamer_gunicorn.py?flask_example:app
這樣每個(gè)請求會負(fù)載均衡到每個(gè) web server 中進(jìn)行 CPU 預(yù)處理,然后均勻的分布到 GPU worke 中進(jìn)行模型 predict。
Future API
from?service_streamer?import?ThreadedStreamerstreamer?=?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)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)
基準(zhǔn)測試
我們使用 wrk 來使做基準(zhǔn)測試。?
環(huán)境
單個(gè)GPU進(jìn)程
#?start?flask?threaded?serverpython?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
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
多個(gè)GPU進(jìn)程?
這里對比單 web server 進(jìn)程的情況下,多 GPU worker 的性能,驗(yàn)證通過和負(fù)載均衡機(jī)制的性能損耗。Flask 多線程 server 已經(jīng)成為性能瓶頸,故采用 gevent server。
利用Future API使用多個(gè)GPU?
為了規(guī)避 web server 的性能瓶頸,我們使用底層 Future API 本地測試多 GPU worker 的 benchmark。
可以看出 service_streamer 的性能跟 GPUworker 數(shù)量及乎成線性關(guān)系,其中進(jìn)程間通信的效率略高于 redis 通信。
點(diǎn)擊以下標(biāo)題查看更多往期內(nèi)容:?
KDD Cup 2019 AutoML Track冠軍團(tuán)隊(duì)技術(shù)分享
神經(jīng)網(wǎng)絡(luò)架構(gòu)搜索(NAS)綜述 | 附資料推薦
小米拍照黑科技:基于NAS的圖像超分辨率算法
深度解讀:小米AI實(shí)驗(yàn)室最新成果FairNAS
自動機(jī)器學(xué)習(xí)(AutoML)最新綜述
NAS-FPN:基于自動架構(gòu)搜索的特征金字塔網(wǎng)絡(luò)
#投 稿 通 道#
?讓你的論文被更多人看到?
如何才能讓更多的優(yōu)質(zhì)內(nèi)容以更短路徑到達(dá)讀者群體,縮短讀者尋找優(yōu)質(zhì)內(nèi)容的成本呢?答案就是:你不認(rèn)識的人。
總有一些你不認(rèn)識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學(xué)者和學(xué)術(shù)靈感相互碰撞,迸發(fā)出更多的可能性。
PaperWeekly 鼓勵(lì)高校實(shí)驗(yàn)室或個(gè)人,在我們的平臺上分享各類優(yōu)質(zhì)內(nèi)容,可以是最新論文解讀,也可以是學(xué)習(xí)心得或技術(shù)干貨。我們的目的只有一個(gè),讓知識真正流動起來。
??來稿標(biāo)準(zhǔn):
? 稿件確系個(gè)人原創(chuàng)作品,來稿需注明作者個(gè)人信息(姓名+學(xué)校/工作單位+學(xué)歷/職位+研究方向)?
? 如果文章并非首發(fā),請?jiān)谕陡鍟r(shí)提醒并附上所有已發(fā)布鏈接?
? PaperWeekly 默認(rèn)每篇文章都是首發(fā),均會添加“原創(chuàng)”標(biāo)志
? 投稿郵箱:
? 投稿郵箱:hr@paperweekly.site?
? 所有文章配圖,請單獨(dú)在附件中發(fā)送?
? 請留下即時(shí)聯(lián)系方式(微信或手機(jī)),以便我們在編輯發(fā)布時(shí)和作者溝通
?
現(xiàn)在,在「知乎」也能找到我們了
進(jìn)入知乎首頁搜索「PaperWeekly」
點(diǎn)擊「關(guān)注」訂閱我們的專欄吧
關(guān)于PaperWeekly
PaperWeekly 是一個(gè)推薦、解讀、討論、報(bào)道人工智能前沿論文成果的學(xué)術(shù)平臺。如果你研究或從事 AI 領(lǐng)域,歡迎在公眾號后臺點(diǎn)擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
▽ 點(diǎn)擊 |?閱讀原文?| 訪問項(xiàng)目主頁
總結(jié)
以上是生活随笔為你收集整理的开源项目 | 五分钟搭建BERT服务,实现1000+QPS的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BERT or XLNet,围观NLP巅
- 下一篇: 如何用最简单的方式理解傅立叶变换?