深度學習模型訓練完成後,如何有效率地佈署和提供服務是一個關鍵議題。本文以意圖分類別模型為例,詳細介紹了模型服務化的流程,包含自建預測器和使用 TorchServe 兩種方案。自建預測器方案中,我們利用 gRPC 建立網路介面,並設計模型管理器來載入和管理模型,同時說明瞭模型檔案的組成和載入方式,以及完整的預測工作流程。我們也分析了模型服務程式碼與訓練程式碼的關係,指出模型架構變更時需要同步更新服務程式碼。為瞭解決模型數量龐大導致的記憶體問題,我們引入了模型驅逐的概念,並以 LRU 演算法為例說明其運作原理。最後,我們介紹了 TorchServe 模型伺服器,一個專為 PyTorch 模型設計的服務化工具,並提供了使用 TorchServe 建立預測服務的完整範例,包含 Docker 啟動指令和 gRPC 請求範例。

7.1.4 意圖分類別預測器

在瞭解前端服務及其內部路由邏輯後,我們現在來看這個範例預測服務的最後一部分:後端預測器。為了展示一個完整的深度學習應用案例,我們實作了一個預測器容器來執行第3章中訓練的意圖分類別模型。

這個自建的意圖分類別預測器可以視為一個獨立的微服務,能夠同時服務多個意圖模型。它具有一個gRPC網路介面和一個模型管理器。模型管理器是預測器的核心,負責多項任務,包括載入模型檔案、初始化模型、將模型快取在記憶體中,以及使用使用者輸入執行模型。圖7.3顯示了預測器的設計圖以及預測器內的預測工作流程。

預測工作流程

讓我們以模型A的意圖預測請求為例,來考慮圖7.3中的工作流程。它按照以下步驟執行:

  1. 前端服務中的預測器客戶端呼叫預測器的網路gRPC介面,以使用模型A執行意圖預測。
  2. 模型管理器被呼叫以處理請求。
  3. 模型管理器從共用磁碟卷載入模型A的模型檔案,初始化模型,並將其放入模型快取中。模型檔案應該已經由前端服務放置在共用磁碟捲上。
  4. 模型管理器藉助轉換器的幫助執行模型A,以對輸入和輸出資料進行前處理和後處理。
  5. 傳回預測結果。

圖7.3 後端意圖預測器設計和預測工作流程

  • AUC-ROC end note

@enduml


### 預測API

意圖預測器有一個API——PredictorPredict(參見程式碼清單7.7)。它接受兩個引數,runId和document。runId是模型ID,document是一個文字字串。你可以在grpc-contract/src/main/proto/prediction_service.proto中找到完整的gRPC合約。

```proto
service Predictor {
  rpc PredictorPredict(PredictorPredictRequest) returns (PredictorPredictResponse);
}

message PredictorPredictRequest {
  string runId = 1;
  string document = 2;
}

message PredictorPredictResponse {
  string response = 1;
}

程式碼解密:

  • service Predictor 定義了一個名為Predictor的gRPC服務。
  • rpc PredictorPredict 定義了一個名為PredictorPredict的RPC方法,用於執行預測。
  • PredictorPredictRequestPredictorPredictResponse 分別定義了請求和回應的訊息格式。

模型檔案

每個在我們的模型訓練服務中產生的意圖分類別模型都有三個檔案。manifest.json檔案包含了模型的元資料和資料集標籤;預測器需要這些資訊來將模型的預測結果從整數轉換為有意義的文字字串。model.pth是模型的學習引數;預測器將讀取這些網路引數來設定模型的神經網路以進行模型服務。vocab.pth是用於模型訓練的詞彙檔案,在服務中也是必要的,因為我們需要它來將使用者輸入(字串)轉換為模型輸入(十進位制數)。

{
  "Algorithm": "intent-classification",
  "Framework": "Pytorch",
  "FrameworkVersion": "1.9.0",
  "ModelName": "intent",
  "CodeVersion": "80bf0da",
  "ModelVersion": "1.0",
  "classes": {
    "0": "cancel",
    "1": "ingredients_list",
    "2": "nutrition_info",
    "3": "greeting",
    // ...
  }
}

程式碼解密:

  • manifest.json 包含了模型的元資料,如演算法、框架、版本等。
  • classes 部分定義了類別標籤,用於將模型的輸出對映到具體的類別名稱。

儲存PyTorch模型時,有兩種選擇:序列化整個模型或只序列化學習到的引數。第一種選擇會序列化整個模型物件,包括其類別和目錄結構,而第二種選擇只儲存模型的學習引數。PyTorch團隊建議只儲存模型的學習引數(模型的state_dict)。

torch.save(model.state_dict(), model_local_path)

程式碼解密:

  • 這行程式碼儲存了模型的學習引數到 model_local_path 指定的路徑。

在預測器中,我們使用程式碼清單7.8中所示的模型架構來載入模型檔案——model.pth(僅引數)。服務中的模型執行程式碼源自模型訓練程式碼。

7.1 模型服務範例詳解

在我們的訓練程式碼(training-code/text-classification/train.py)中,你會發現模型服務程式碼與訓練程式碼的模型定義部分幾乎相同。這是因為模型服務本質上是執行訓練好的模型。

class TextClassificationModel(nn.Module):
    def __init__(self, vocab_size, embed_dim, fc_size, num_class):
        super(TextClassificationModel, self).__init__()
        self.embedding = nn.EmbeddingBag(vocab_size, embed_dim, sparse=True)
        self.fc1 = nn.Linear(embed_dim, fc_size)
        self.fc2 = nn.Linear(fc_size, num_class)
        self.init_weights()

    def forward(self, text, offsets):
        embedded = self.embedding(text, offsets)
        return self.fc2(self.fc1(embedded))

內容解密:

  1. 模型架構定義:上述程式碼定義了一個名為 TextClassificationModel 的類別,繼承自 PyTorch 的 nn.Module。這個類別實作了一個簡單的文字分類別模型。
  2. __init__ 方法:初始化模型層,包括嵌入層 (nn.EmbeddingBag) 和兩個全連線層 (nn.Linear)。nn.EmbeddingBag 用於處理變長度的輸入序列。
  3. forward 方法:定義了模型的前向傳播邏輯,將輸入的文字張量和偏移張量透過嵌入層和全連線層,最終輸出分類別結果。

你可能會疑惑訓練程式碼和模型服務程式碼是否需要保持一致。當訓練程式碼發生變化時,模型服務程式碼是否也需要調整?其實,這取決於變化的內容。如果只是訓練策略、超引數調整等,模型服務程式碼不需要改變,因為這些變化隻影響模型的權重和偏差檔案。然而,當神經網路架構發生變化時,模型服務程式碼就需要同步更新。

模型管理器(Model Manager)詳解

模型管理器是意圖預測器的核心元件,負責載入模型檔案並執行模型預測。

class ModelManager:
    def __init__(self, config, tokenizer, device):
        self.model_dir = config.MODEL_DIR
        self.models = {}

    def load_model(self, model_id):
        # 載入模型檔案,包括詞彙表、預測類別對映等
        vocab_path = os.path.join(self.model_dir, model_id, "vocab.pth")
        manifest_path = os.path.join(self.model_dir, model_id, "manifest.json")
        model_path = os.path.join(self.model_dir, model_id, "model.pth")
        vocab = torch.load(vocab_path)
        with open(manifest_path, 'r') as f:
            manifest = json.loads(f.read())
        classes = manifest['classes']
        # 初始化模型並載入權重
        num_class, vocab_size, emsize = len(classes), len(vocab), 64
        model = TextClassificationModel(vocab_size, emsize, self.config.FC_SIZE, num_class).to(self.device)
        model.load_state_dict(torch.load(model_path))
        model.eval()
        self.models[self.model_key(model_id)] = model
        self.models[self.model_vocab_key(model_id)] = vocab
        self.models[self.model_classes(model_id)] = classes

    def predict(self, model_id, document):
        # 從快取中取出模型、詞彙表和類別對映
        model = self.models[self.model_key(model_id)]
        vocab = self.models[self.model_vocab_key(model_id)]
        classes = self.models[self.model_classes(model_id)]
        # 處理輸入文字並進行預測
        processed_text = torch.tensor(text_pipeline(document), dtype=torch.int64)
        offsets = [0, processed_text.size(0)]
        offsets = torch.tensor(offsets[:-1]).cumsum(dim=0)
        val = model(processed_text, offsets)
        res_index = val.argmax(1).item()
        res = classes[str(res_index)]
        return res

內容解密:

  1. ModelManager 類別:負責管理模型的載入和預測。
  2. load_model 方法:根據 model_id 載入對應的模型檔案,包括詞彙表、類別對映和模型權重,並將它們存入快取。
  3. predict 方法:根據輸入的 documentmodel_id,使用快取中的模型進行預測,並傳回預測結果。

意圖預測器預測請求工作流程

意圖預測器的核心工作流程包括以下步驟:

  1. PredictorServicer 註冊到 gRPC 伺服器,使前端服務能夠遠端呼叫預測 API。
  2. 當前端服務呼叫 PredictorPredict API 時,模型管理器會載入對應的模型並執行預測,將結果傳回給前端服務。
def serve():
    model_manager = ModelManager(config, tokenizer=get_tokenizer('basic_english'), device="cpu")
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    prediction_service_pb2_grpc.add_PredictorServicer_to_server(PredictorServicer(model_manager), server)

class PredictorServicer(prediction_service_pb2_grpc.PredictorServicer):
    def __init__(self, model_manager):
        self.model_manager = model_manager

    def PredictorPredict(self, request, context: grpc.ServicerContext):
        self.model_manager.load_model(model_id=request.runId)
        # 省略預測邏輯

內容解密:

  1. serve 函式:初始化 ModelManager 和 gRPC 伺服器,並將 PredictorServicer 註冊到伺服器。
  2. PredictorServicer 類別:實作了 PredictorPredict 方法,該方法會呼叫 ModelManagerload_model 方法載入模型,並執行預測。

模型服務實踐中的模型驅逐與 TorchServe 模型伺服器範例

在前一節中,我們設計了一個自建的預測服務,並討論了其優缺點。本文將重點介紹模型驅逐(Model Eviction)的概念,以及如何使用 TorchServe 模型伺服器來建立預測服務。

模型驅逐

在預測服務的設計中,模型管理器(Model Manager)負責載入和管理模型。然而,當模型數量龐大時,模型管理器可能會耗盡記憶體。因此,需要引入模型驅逐機制,以移除不常用的模型檔案。

LRU 演算法

一種常見的模型驅逐演算法是 LRU(Least Recently Used)演算法。LRU 演算法會將最近使用的模型保留在記憶體中,而將最少使用的模型驅逐出去。

TorchServe 模型伺服器範例

在本文中,我們將使用 TorchServe 模型伺服器來建立預測服務。TorchServe 是一個專為 PyTorch 模型設計的模型服務工具。

服務設計

TorchServe 模型伺服器的設計與前一節的自建預測服務相似。主要的差異在於預測服務的後端被替換為 TorchServe 伺服器。

系統總覽與模型伺服器端對端工作流程
  • AUC-ROC end note

@enduml


#### 前端服務

前端服務的設計保持不變,透過註冊不同的預測器後端來支援不同的模型演算法。當接收到預測請求時,前端服務會根據請求的模型演算法型別將請求路由到相應的預測器後端。

##### 前端服務設計與模型服務工作流程

```plantuml
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title 意圖分類模型服務架構

package "前端服務" {
    component [gRPC 介面] as grpc
    component [路由邏輯] as router
    component [預測器客戶端] as client
}

package "後端預測器" {
    component [模型管理器] as manager
    component [模型快取] as cache
    component [轉換器] as transformer
}

package "模型儲存" {
    component [共用磁碟卷] as disk
    component [模型檔案] as model_file
    component [LRU 驅逐策略] as lru
}

package "服務方案" {
    component [自建預測器] as custom
    component [TorchServe] as torchserve
    component [Docker 容器] as docker
}

grpc --> router
router --> client
client --> manager

manager --> cache
manager --> transformer
manager --> disk

disk --> model_file
cache --> lru

custom --> grpc
torchserve --> docker

note right of manager
  模型管理器功能:
  - 載入模型檔案
  - 初始化模型
  - 執行預測
end note

note right of lru
  LRU 驅逐策略:
  - 解決記憶體問題
  - 移除最少使用模型
  - 動態快取管理
end note

@enduml
  • AUC-ROC end note

@enduml


### 啟動服務與傳送預測請求

要啟動 TorchServe 後端和預測服務,可以使用以下命令:

```bash
# step 1: start torchserve backend
docker run --name intent-classification-torch-predictor\
--network orca3 --rm -d -p "${ICP_TORCH_PORT}":7070 \
-p "${ICP_TORCH_MGMT_PORT}":7071 \
-v "${MODEL_CACHE_DIR}":/models \
-v "$(pwd)/config/torch_server_config.properties": \
/home/model-server/config.properties \
pytorch/torchserve:0.5.2-cpu torchserve \
--start --model-store /models

# step 2: start the prediction service (the web frontend)
docker build -t orca3/services:latest -f services.dockerfile .
docker run --name prediction-service --network orca3 \
--rm -d -p "${PS_PORT}":51001 \
-v "${MODEL_CACHE_DIR}":/tmp/modelCache \
orca3/services:latest \
prediction-service.jar

# step 3: make a prediction request, ask intent for “merry christmas”
grpcurl -plaintext \
-d "{
\"runId\": \"${MODEL_ID}\",
\"document\": \"merry christmas\"
}" \
localhost:"${PS_PORT}" prediction.PredictionService/Predict

程式碼解析

上述程式碼展示瞭如何啟動 TorchServe 後端和預測服務,並傳送預測請求。

  1. 啟動 TorchServe 後端:使用 Docker 執行 TorchServe 後端容器,並對映必要的埠和卷。
    • -p "${ICP_TORCH_PORT}":7070-p "${ICP_TORCH_MGMT_PORT}":7071 分別對映 TorchServe 的服務埠和管理埠。
    • -v "${MODEL_CACHE_DIR}":/models 將本地的模型快取目錄掛載到容器內的 /models 目錄。
  2. 啟動預測服務:使用 Docker 執行預測服務容器,並對映必要的埠和卷。
    • -v "${MODEL_CACHE_DIR}":/tmp/modelCache 將本地的模型快取目錄掛載到容器內的 /tmp/modelCache 目錄。
  3. 傳送預測請求:使用 grpcurl 命令向預測服務傳送 gRPC 請求,詢問 “merry christmas” 的意圖。

詳細解析:

  • 每個步驟的作用與邏輯都在程式碼段後給予了詳細的「#### 內容解密:」說明。
  • 在此範例中,我們明確展示瞭如何使用 TorchServe 和 Docker 建立一個可擴充套件的預測服務。