TorchServe 提供簡化的模型佈署流程,只需設定 Docker 容器和磁碟分享,即可快速建立模型服務,無需額外程式碼。相較於自建預測器,TorchServe 更具彈性,支援所有 PyTorch 訓練的模型,不限於特定演算法。然而,TorchServe 要求使用其 API 進行模型服務請求,並以特定格式封裝模型檔案。本文將以意圖分類別模型為例,深入解析 TorchServe 的模型註冊與推論 API,並說明如何建立符合規範的 .mar 檔案。

透過 torch-model-archiver 工具或 Python 程式函式庫,可以將模型檔案封裝成 .mar 格式。.mar 檔案包含模型權重、MANIFEST.json 描述模型中繼資料,以及 torchserve_handler.py 定義模型處理邏輯。torchserve_handler.py 負責模型初始化、輸入預處理、推論執行和輸出後處理,確保 TorchServe 能夠正確載入和執行模型。瞭解 .mar 檔案結構和 API 使用方式,是成功佈署模型的關鍵。

7.2 TorchServe 模型伺服器範例

7.2.4 TorchServe 後端

TorchServe 是由 PyTorch 團隊開發的工具,用於服務 PyTorch 模型。TorchServe 作為一個黑盒執行,提供 HTTP 和 gRPC 介面,用於模型預測和內部資源管理。圖 7.6 可視化了我們在此範例中如何使用 TorchServe 的工作流程。

在我們的範例程式碼中,我們將 TorchServe 作為 Docker 容器執行,該容器由 PyTorch 團隊提供,然後將本地檔案目錄掛載到容器中。這個檔案目錄作為 TorchServe 程式的模型儲存。在圖 7.6 中,我們採取三個步驟來執行模型預測。首先,我們將 PyTorch 模型檔案複製到模型儲存目錄。其次,我們呼叫 TorchServe 管理 API 將模型註冊到 TorchServe 程式。最後,我們呼叫 TorchServe API 來執行模型的預測——在我們的例子中,是意圖分類別模型。

與第 7.1.4 節中的自建意圖預測器相比,TorchServe 簡單得多。我們可以在不寫任何程式碼的情況下使模型服務工作;我們只需要設定一個具有磁碟分享的 Docker 容器。另外,與僅適用於意圖分類別演算法的意圖預測器不同,TorchServe 不與任何特定的訓練演算法繫結;只要是使用 PyTorch 框架訓練的模型,它都可以服務。

TorchServe 提供的靈活性和便利性伴隨著一些要求。TorchServe 要求操作員使用自己的 API 集傳送模型服務請求,並且還要求模型檔案以 TorchServe 格式封裝。讓我們在接下來的兩個小節中看看這些要求。

7.2.5 TorchServe API

TorchServe 提供多種型別的 API,例如健康檢查、模型說明、模型服務、工作者管理和模型註冊。每個 API 有兩種型別的實作:HTTP 和 gRPC。由於 TorchServe 在其官方網站(https://pytorch.org/serve/)和 GitHub 儲存函式庫(https://github.com/pytorch/serve)上有非常詳細的 API 合約和用法說明,您可以在那裡找到詳細資訊。在本文中,我們將重點介紹在我們的範例服務中使用的模型註冊和模型推理 API。

MODEL REGISTRATION API

因為 TorchServe 對模型服務採取黑盒方法,所以在使用模型之前需要先註冊它。更具體地說,在我們將模型檔案放在 TorchServe 的模型儲存(本地檔案目錄)之後,TorchServe 不會自動載入模型。我們需要向 TorchServe 註冊模型檔案和模型的執行方法,以便 TorchServe 知道如何與此模型一起工作。

在我們的程式碼範例中,我們使用 TorchServe 的 gRPC 模型註冊 API 從預測服務註冊我們的意圖模型,如下面的程式碼片段所示:

public void registerModel(GetArtifactResponse artifact) {
    String modelUrl = String.format(MODEL_FILE_NAME_TEMPLATE, artifact.getRunId());
    String torchModelName = String.format(TORCH_MODEL_NAME_TEMPLATE, artifact.getName(), artifact.getVersion());
    ManagementResponse r = managementStub.registerModel(
        RegisterModelRequest.newBuilder()
            .setUrl(modelUrl)
            .setModelName(torchModelName)
            .build());
    // 為此模型分配資源(TorchServe 工作者)
    managementStub.scaleWorker(ScaleWorkerRequest.newBuilder()
        .setModelName(torchModelName)
        .setMinWorker(1)
        .build());
}

內容解密:

  1. registerModel 方法負責向 TorchServe 註冊模型。
  2. modelUrl 是模型的 URL,torchModelName 是模型的名稱,格式為 {name}-{version}
  3. managementStub.registerModel 方法用於註冊模型,提供模型的 URL 和名稱。
  4. managementStub.scaleWorker 方法用於為註冊的模型分配工作者資源,設定最小工作者數量為 1。

TorchServe 的模型檔案已經包含了模型的後設資料,包括模型版本、模型執行時和模型服務入口點。因此,在註冊模型時,我們通常只需要在 registerModel API 中設定模型檔案名稱。此外,我們還可以使用 scaleWorker API 控制為此模型分配的計算資源量。

MODEL INFERENCE API

TorchServe 提供統一的模型服務 API,用於多樣化的模型;這使得 TorchServe 簡單易用。要執行預設版本的模型的預測,請向 POST /predictions/{model_name} 傳送 REST 請求。要執行已載入模型的特定版本的預測,請向 POST /predictions/{model_name}/{version} 傳送 REST 請求。預測請求中的待預測內容以二進位制格式輸入。例如:

# 對 resnet-18 模型的單一輸入進行預測
curl http://localhost:8080/predictions/resnet-18 \
-F "data=@kitten_small.jpg"

# 對 squeezenet1_1 多輸入進行預測
curl http://localhost:8080/predictions/squeezenet1_1 \
-F 'data=@docs/images/dogs-before.jpg' \
-F 'data=@docs/images/kitten_small.jpg'

在我們的範例服務中,我們使用 gRPC 介導向 TorchServe 傳送預測請求。程式碼清單 7.12 展示了 TorchGrpcPredictorBackend 使用者端將前端 API 呼叫的預測請求轉換為 TorchServe 後端 gRPC 呼叫。

// 呼叫 TorchServe gRPC 預測 API
public String predict(GetArtifactResponse artifact, String document) {
    return stub.predictions(PredictionsRequest.newBuilder()
        .setModelName(String.format(TORCH_MODEL_NAME_TEMPLATE, artifact.getName(), artifact.getVersion()))
        .putAllInput(ImmutableMap.of("data", ByteString.copyFrom(document, StandardCharsets.UTF_8)))
        .build()).getPrediction().toString(StandardCharsets.UTF_8);
}

內容解密:

  1. predict 方法負責呼叫 TorchServe 的 gRPC 預測 API。
  2. stub.predictions 方法用於傳送預測請求,提供模型的名稱和輸入資料。
  3. 輸入資料被轉換為二進位制格式,並透過 ByteString.copyFrom 方法傳遞給 PredictionsRequest
  4. 傳回的預測結果被轉換為字串並傳回。

7.2.6 TorchServe 模型檔案

到目前為止,您已經瞭解了 TorchServe 的模型服務工作流程和 API。您可能會想知道當 TorchServe 對其服務的模型一無所知時,模型服務是如何工作的。在第 6 章中,我們瞭解到要服務一個模型,預測服務需要知道模型的演算法和模型的輸入/輸出模式。出乎意料的是,TorchServe 可以在不知道模型的演算法和模型的輸入/輸出資料格式的情況下執行模型服務。訣竅在於 TorchServe 的模型檔案。

TorchServe 要求將模型封裝成特殊的 .mar 檔案。我們可以使用 torch-model-archiver CLI 或 model_archiver Python 函式庫將 PyTorch 模型檔案封裝成 .mar 檔案。

要存檔一個 TorchServe .mar 檔案,我們需要提供模型的名稱、模型檔案(.pt.pth)和一個處理程式檔案。處理程式檔案是關鍵部分;它是一個 Python 程式碼檔案,定義了處理自定義 TorchServe 推理邏輯的邏輯。因為 TorchServe 的模型包(.mar 檔案)包含了模型的演算法、模型的資料和模型的執行程式碼,並且模型的執行程式碼遵循 TorchServe 的預測介面(協定),所以 TorchServe 可以透過其通用的預測 API 執行任何模型(.mar 檔案),而無需瞭解模型的演算法。

當 TorchServe 接收到一個預測請求時,它將首先找到內部的工作者程式,該程式託管該模型,然後觸發模型的處理程式檔案來處理請求。處理程式檔案包含四個部分的邏輯:模型網路初始化、輸入資料預處理、模型推理和預測結果後處理。為了使前面的解釋更加具體,讓我們看看我們的意圖模型的例子。

意圖模型的 handler.py 範例

from ts.torch_handler.base_handler import BaseHandler

class IntentHandler(BaseHandler):
    def initialize(self, ctx):
        # 初始化模型的邏輯
        pass
    
    def preprocess(self, data):
        # 輸入資料預處理邏輯
        pass
    
    def inference(self, data):
        # 執行模型推理的邏輯
        pass
    
    def postprocess(self, data):
        # 處理預測結果的邏輯
        pass

內容解密:

  1. 自定義的 IntentHandler 繼承自 BaseHandler
  2. initialize 方法用於初始化模型的邏輯。
  3. preprocess 方法負責輸入資料的預處理。
  4. inference 方法執行模型的推理。
  5. postprocess 方法處理模型的預測結果。

透過這種方式,TorchServe 能夠支援多種不同的 PyTorch 模型,只要它們被正確地封裝成 .mar 檔案並提供了適當的處理程式程式碼。這種設計使得 TorchServe 成為一個非常靈活和通用的模型服務平台。

7.2 TorchServe 模型伺服器範例:意圖分類別模型佈署實務

深入解析 .mar 檔案結構及其重要性

當我們檢視範例服務中的意圖模型 .mar 檔案時,會發現除了模型檔案外,還額外包含了 MANIFEST.jsontorchserve_handler.py 兩個關鍵檔案。這兩個檔案對於 TorchServe 正確載入和執行模型預測至關重要。以下展示了一個典型的意圖分類別 .mar 檔案的資料夾結構:

intent.mar
├── MAR-INF
│   └── MANIFEST.json
├── manifest.json
├── model.pth
├── torchserve_handler.py
└── vocab.pth

MANIFEST.json:模型中繼資料的定義

MANIFEST.json 檔案定義了模型的元資料,包括模型版本、模型權重檔案、模型名稱以及處理器(handler)檔案。這個檔案讓 TorchServe 能夠在不瞭解模型實作細節的情況下載入並執行任意模型的預測。

{
  "createdOn": "09/11/2021 10:26:59",
  "runtime": "python",
  "model": {
    "modelName": "intent_80bf0da",
    "serializedFile": "model.pth",
    "handler": "torchserve_handler.py",
    "modelVersion": "1.0"
  },
  "archiverVersion": "0.4.2"
}

內容解密:

  1. createdOn:記錄 .mar 檔案建立的時間戳記。
  2. runtime:指定執行環境,此處為 Python。
  3. modelNameserializedFile:分別指定了模型名稱和權重檔案名稱。
  4. handler:指向處理模型預測請求的 Python 指令碼,即 torchserve_handler.py
  5. modelVersion:模型的版本資訊,有助於版本控制和更新。

torchserve_handler.py:自訂模型處理邏輯

當模型在 TorchServe 中註冊後,TorchServe 會呼叫 torchserve_handler.py 中的 handle(self, data, context) 函式作為模型預測的入口點。這個處理器檔案管理了整個模型服務流程,包括模型的初始化、輸入請求的預處理、模型執行以及預測結果的後處理。

程式碼重點解析:

class ModelHandler(BaseHandler):
    def initialize(self, ctx):
        # 載入模型相關檔案(權重、詞彙表等)並初始化模型
        model_dir = properties.get("model_dir")
        model_path = os.path.join(model_dir, "model.pth")
        vacab_path = os.path.join(model_dir, "vocab.pth")
        # 載入詞彙表和模型設定檔
        self.vocab = torch.load(vacab_path)
        with open(manifest_path, 'r') as f:
            self.manifest = json.loads(f.read())
        # 初始化模型並載入權重
        self.model = self.TextClassificationModel(vocab_size, emsize, self.fcsize, num_class).to("cpu")
        self.model.load_state_dict(torch.load(model_path))
        self.model.eval()
        self.initialized = True

    def preprocess(self, data):
        # 將原始輸入轉換為模型可接受的格式
        preprocessed_data = data[0].get("data")
        text_pipeline = lambda x: self.vocab(self.tokenizer(x))
        user_input = " ".join(str(preprocessed_data))
        processed_text = torch.tensor(text_pipeline(user_input), dtype=torch.int64)
        offsets = [0, processed_text.size(0)]
        offsets = torch.tensor(offsets[:-1]).cumsum(dim=0)
        return (processed_text, offsets)

    def inference(self, model_input):
        # 執行模型推斷
        model_output = self.model.forward(model_input[0], model_input[1])
        return model_output

    def postprocess(self, inference_output):
        # 對模型輸出進行後處理,轉換為所需的格式
        res_index = inference_output.argmax(1).item()
        classes = self.manifest['classes']
        postprocess_output = classes[str(res_index)]
        return [{"predict_res": postprocess_output}]

    def handle(self, data, context):
        # 模型服務入口點,依次呼叫預處理、推斷和後處理
        model_input = self.preprocess(data)
        model_output = self.inference(model_input)
        return self.postprocess(model_output)

內容解密:

  1. initialize 方法:負責載入模型權重、詞彙表等相關檔案,並初始化模型例項。
  2. preprocess 方法:將原始輸入資料轉換為模型可接受的張量格式。
  3. inference 方法:執行模型的推斷,輸出預測結果。
  4. postprocess 方法:將模型的輸出結果轉換為所需的格式,例如類別標籤。
  5. handle 方法:整個模型服務流程的入口點,依次呼叫預處理、推斷和後處理方法。

封裝 .mar 檔案的最佳實踐

在決定使用 TorchServe 進行模型服務時,建議在訓練階段就生成 .mar 檔案。有兩種方法可以實作這一點:

  1. 使用 torch-model-archiver 命令列工具。
  2. 在訓練程式碼中直接呼叫 model_archiver.archive 方法。

方法一:使用命令列工具封裝模型

torch-model-archiver --model-name intent_classification --version 1.0 \
--model-file torchserve_model.py --serialized-file workspace/MiniAutoML/{model_id}/model.pth \
--handler torchserve_handler.py --extra-files workspace/MiniAutoML/{model_id}/vocab.pth,workspace/MiniAutoML/{model_id}/manifest.json

方法二:在訓練程式碼中封裝模型

model_archiver.archive(model_name=archive_model_name,
                       handler_file=handler, 
                       model_state_file=model_local_path,
                       extra_files=extra_files, 
                       model_version=config.MODEL_SERVING_VERSION,
                       dest_path=config.JOB_ID)

內容解密:

  • 兩種方法都能有效地將模型及其依賴檔案封裝成 .mar 檔案,供 TorchServe 使用。
  • 第一種方法適合在訓練完成後手動封裝,而第二種方法則可以無縫整合到訓練流程中,自動生成 .mar 檔案。