1. 程式人生 > >TensorFlow Serving的原理和程式碼實現

TensorFlow Serving的原理和程式碼實現

本文介紹tensorflow Serving的原理和程式碼實現, 並提供簡要的程式碼閱讀指導.

如何serve一個模型

具體的步驟可以參考官方文件. 主要包括兩個部分:
1. 匯出模型
1. 啟動服務

需要說明的是匯出模型部分. 如果要把我們訓練的模型拿來提供服務, 除了模型本身外, 還需要一些額外的資訊, 比如模型的名稱, 輸入、輸出對應的tensor資訊, 方法名, 這些東西可以讓TFS進行請求資料的格式檢查以及目標模型查詢. 這就是模型匯出的作用. 直接拿一個checkpoint檔案之類的是不能用的. TF使用SavedModel格式匯出模型, 並提供了相關的工具(tf.saved_model.builder.SavedModelBuilder).

TFS的功能

  • 支援多種模型服務策略,比如用最新版本/所有版本/指定版本, 以及動態策略更新、模型的增刪等
  • 自動載入/解除安裝模型
  • Batching
  • 多種平臺支援(非TF平臺)

TFS的架構

架構圖

本節簡要介紹各模組的主要功能, 後續章節介紹他們相互之間是如何協作的.

Servable

Servable是對模型的抽象, 但是任何能夠提供演算法或者資料查詢服務的實體都可以是Servable, 不一定是機器學習模型.

在我們常用的場景下, Servable就是模型. 所以本文有時會混用模型和Servable.

ServerCore

整個服務系統的建立維護, 建立http rest server、grpc server和模型管理部分(AspiredVersionManager)之間的關係等.

AspiredVersionManager

模型管理的上層控制部分. 負責執行Source發出的模型管理指令. 一部分功能通過回撥的方式由Source呼叫, 一部分由獨立執行緒執行.

BasicManager

負責Servable的管理, 包括載入、解除安裝、狀態查詢、資源跟蹤等, 對外提供如下介面操作Servable
- ManageServable
- LoadServable
- UnloadServable
- StopManagerServable

另外提供介面查詢servableHandle(GetUntypedServableHandle), 也就是載入好的模型,供http rest或者grpc server呼叫進行推理.

所有受管理的servable都放在ManagedMap裡面, 已經正常載入的servable同時也放在ServingMap裡進行管理, 提供查詢介面.

LoaderHarness

LoaderHarness對Loader提供狀態跟蹤, ServingMap和ManagedMap裡面儲存的都是LoaderHarness物件,只有通過LoaderHarness才能訪問Loader的介面.

Loader

Loader對Servable的生命週期進行控制, 包括load/unload介面,資源預估介面等. 載入之後的Servable也存在Loader裡面.

Adapter

Adapter是為了將Source(比如檔案系統)轉換成Loader而引入的抽象, 這樣server core的實現和具體的平臺解耦.

SourceRouter

Adapter是平臺相關的, 每個平臺一個Adapter, 但是Source是和Servable相關的, 這樣在Adapter和Source之間存在一對多的關係, Router負責維護這些對應關係.

Source

Source是對Servable的來源(Source)的抽象, 比如模型檔案是某個模型的Source. Source監控外部的資源(如檔案系統), 發現新的模型版本, 並通知Target.

Target

Target是和Source對應的抽象概念, AspiredVersionManager、Router都是Target.

啟動過程

TFS啟動的全部引數可以參考main.c. 主要的引數包括服務埠(gprc和http rest埠)和模型配置. 其中模型配置可以直接指定(名稱(model_name)、路徑(model_base_path)等), 也可以使用檔案指定(model_config_file,格式參考model_server_config.proto). 如果只啟動單個模型的服務可以使用引數指定, 如果是多個模型必須使用檔案. 其他的引數可以使用預設值.

啟動過程主要是建立ServerCore物件, 並啟動grpc server和http server.

ServerCore物件可以認為是系統中樞, 模型的維護, 服務請求的處理都是由他完成. ServerCore通過BasicManager管理所有的model(多版本號), 並查處模型已經提供預測、分類、迴歸請求.

ServerCore啟動的時候建立AspiredVersionManager, AspiredVersionManager會啟動定時任務(執行緒), 用於處理AspiredVersionRequest訊息, 其實就是模型的載入、解除安裝.

啟動的時候ServerCore還會根據模型配置建立檔案系統掃描任務, 定時掃描模型檔案目錄並進行相應的處理

http rest服務啟動後, 會監聽http post請求, 將請求(json)轉換成protobuf格式的訊息, 通過serverCore查詢對應的模型版本, 獲取對應的已載入的模型, 進行運算並返回結果.

rgpc服務與 http rest服務類似.

模型維護

檔案系統掃描

Source是TFS定義的對未載入模型物件的抽象, 目前實現了兩種Source, 一種是StaticStoragePathSource,一種是FileSystemStoragePathSource. 前者是簡單的靜態的模型檔案儲存系統, 僅僅在啟動時觸發模型的載入, 沒有其他動作. 後者是動態的Source, 能監測儲存系統的變化併發出通知.

TFS實現Source時將模組職責劃分的很清晰, Source的職責就是監測變化, 如何處理則由Source的使用者決定, 所以Source有一個介面SetAspiredVersionsCallback, 可以設定回撥函式用於通知AspiredVersion的變化. Source在變化的時候就會呼叫設定的回撥函式.

作為Source的對等物件, 系統也定義了Target, 有介面GetAspiredVersionsCallback, 用於獲取處理AspiredVersions的回撥介面, 然後我們就可以將Target和Source連起來了.

template <typename T>
void ConnectSourceToTarget(Source<T>* source, Target<T>* target) {
  source->SetAspiredVersionsCallback(target->GetAspiredVersionsCallback());
}

Source和ServerCore的關係是這樣的

Source --> Router --> Adapter --> AspiredVersionManager

上述連線關係裡面, Router和Adapter既是Source又是Target, AspiredVersionManager是Target. 但是Router沒有實現Source介面, 而是要求在建立Router物件時直接將Adapter作為引數, 這樣實現主要目的是建立一對多的關係.

系統根據所支援平臺的個數(tensorflow算是一種平臺)建立Adapter, 一種平臺對應一個Adapter, 負責建立模型載入器Loader. 對於tensorflow平臺, 對應的adapter是SavedModelBundleSourceAdapter.

Router負責根據模型名稱查詢對應的平臺(model_config裡面有指定平臺名稱), 從而定位到對應的Adapter.

這些連線關係是在系統啟動, 或者更新model-config的時候建立的.

預設配置下, FileSystemStoragePathSource為Source的例項, SavedModelBundleSourceAdapter為Adapter的例項, DynamicSourceRouter為Router的例項.

  1. FileSystemStoragePathSource有自己單獨的工作執行緒, 週期查詢檔案系統, 發現每個模型的版本, 根據指定的servable_version_policy(model_config), 建立ServableData(模型名, 版本號, 路徑), 傳給Router
  2. Router根據路由找到對應的adapter, 傳給Adataper
  3. Adapter將ServableData(模型名, 版本號, 路徑)轉換成ServableData(模型名, 版本, Loader), 傳給AspiredVersionManager
  4. AspiredVersionManager將這些資訊存到pending_aspired_versions_requests_, 等待另外一個工作執行緒(AspiredVersionsManager_ManageState_Thread)處理

上述訊息傳遞的方式是依次呼叫下游的SetAspiredVersions函式.

模型載入/解除安裝

上節提到的工作執行緒ManageState_Thread是在AspiredVersionsManager建立的時候啟動的定時執行緒, 負責處理pending_aspired_versions_requests_裡面的ServableData.

manage_state_thread_.reset(new PeriodicFunction(
        [this]() {
          this->FlushServables();
          this->HandlePendingAspiredVersionsRequests();
          this->InvokePolicyAndExecuteAction();
        },
        manage_state_interval_micros));

該執行緒的工作分3部分, 如上述程式碼所示

  1. FlushServables主要目的是將異常狀態的模型清理掉, 或者停止載入.

  2. HandlePendingAspiredVersionsRequests取出每個模型的資訊分別處理, 如果發現當前要載入的模型版本已經存在, 需要等待之前的模型完成服務並退出, 這叫re-aspired version. 如果不是這種情況, 計算需要載入的模型和需要解除安裝的模型, 將新載入的模型管理起來(加到管理列表),將需要解除安裝的模型打上標記並停止其載入.

  3. InvokePolicyAndExecuteAction每次只會執行一個模型的一個動作(load/unload). 具體方法是每個模型根據aspired_version_policy(AvailabilityPreservingPolicy/ResourcePreservingPolicy)選擇一個動作, 然後所有模型的選擇動作放在一起排序, unload優先load, 決定處理哪一個模型. 執行動作的時候, 會呼叫Loader的相應函式, 並設定相關的狀態(參考模型狀態管理).

可以看出ManageState_Thread並不是一股腦的進行模型的載入、解除安裝等操作, 而是兼顧了資源佔用、服務可用性、系統負荷的, 考慮周到.

模型管理

AspiredVersionManager的成員BasicManager負責模型管理, 把一個模型版本加入到BasicManager裡面就叫管理一個模型(manager a model). BasicManager通過LoaderHarness間接管理模型, LoaderHarness管理一個模型的生命週期, 持有模型的Loader物件, 在合適的時候呼叫Loader的Load/Unload完成狀態遷移.

LoaderHarness有自己的狀態記錄, 每次執行動作時都會進行狀態遷移.

進行模型狀態管理的同時, BasicManager還會將模型的服務狀態釋出到EventBus(servable_event_bus_), 便於其他模組對這些狀態變化進行訂閱.

快速模型載入

首次啟動的時候, 採用快速載入模式, 實現方法是臨時增加模型載入執行緒(4倍可用cpu個數). 完成載入後恢復執行緒數.
程式碼

    // The number of load threads used to load the initial set of models at
    // server startup. This is set high to load up the initial set of models
    // fast, after this the server uses num_load_threads.
    int32 num_initial_load_threads = 4.0 * port::NumSchedulableCPUs();

Aspired version policy

AspiredVersionPolicy是用來決定一個模型的多個版本誰先處理, AvailabilityPreservingPolicy的目標是保證服務可用, 會臨時犧牲一些資源, 而
ResourcePreservingPolicy是優先保證佔用更少的資源, 可能會犧牲服務可用性.

程式碼裡面的註釋提供了很好的解釋, 可參考.

Servable Version Policy

ServableVersionPolicy
定義了模型的多個版本如何進行選擇. 注意和Aspired Version Policy的關係, 一個是如何選擇版本, 一個是選擇了版本後, 如何選擇執行先後順序.

目前提供3種方式:
- all 所有的版本
- latest 最新的N個版本
- specific 一個或一些指定的版本號

模型配置動態更新

tfs的main預設並沒有提供模型配置檔案的動態更新, 但是呼叫ServerCore::ReloadConfig(const ModelServerConfig& new_config)就可以完成更新. 可以自己包裝該介面在合適的時間進行呼叫.

動態更新可以增加、刪除模型, 修改版本策略(比如從最新版本到指定某版本)等.

動態更新的實現也不復雜, 更新Router的路由策略, 更新Source就可以了. 程式碼實現可以參考ServerCore::AddModelsViaModelConfigList

TFS目前還不支援動態增加平臺.

資源管理

TFS目前僅支援記憶體資源的管理,類ResourceTracker用來跟蹤當前Servable消耗的總資源,當有新的Servable需要載入的時候,會計算剩下的資源是否夠用, 並預留資源(BasicManager::ReserveResources).

ServerCore.Options.total_model_memory_limit_bytes控制總資源,預設設定無上限.

SavedModelBundleFactory提供了對TF模型資源的評估方法,簡單的將模型檔案大小乘1.2倍(程式碼, EstimateResourceFromPath).

Batching

Batching是提高服務效能的一個有效辦法, 最簡單的batching就是把多個單獨請求打包到一起, 由TF Session一次運算得出結果.

Batching的實現就是在普通的Tensorflow Session之外包裝一個BatchingSession,負責快取、排程. 參考程式碼SavedModelBundleFactory::Create. Batching分兩個部分實現,BatchScheduler和BatchingSession.

BatchingSession對外提供Run和ListDevices介面. 有Run請求的時候, 將請求打包成一個BatchTask,交給BatchScheduler去處理,並等待處理結束,取出結果返回.

BatchScheduler是一個底層的排程器,擁有自己的執行緒池, 負責將多個BatchTask合併處理.

程式碼

Model Warmup

模型載入後,如果需要Warmup,從模型檔案目錄中取出預先存好的請求資料,呼叫模型進行推理,如此可以將模型”熱身”,避免首次處理服務請求時時延過大。

配置

平臺配置

 如果需要使用分散式TF伺服器, 可以在這裡指定. 預設情況下使用本地Session(DirectSession)

- Session 引數
- Batching引數

模型配置

與Tensorflow的關係

TFS對TF是原始碼級別的依賴, 兩者的版本號保持一致, TFS在載入模型、執行推理的過程中, 都是呼叫TF的庫. TFS使用的很多基本構件, 比如多執行緒庫/BatchScheduler, 都是直接使用TF的程式碼.

效能

TFS在如下方面做出了效能提升的設計:
- Batching
- Fast Model Loading
- Model Warmup
- Availability/Resource Proserving Policy