构建标准 TensorFlow ModelServer

本教程将向您展示如何使用 TensorFlow Serving 组件构建标准 TensorFlow ModelServer,该服务器可以动态发现和服务已训练 TensorFlow 模型的新版本。如果您只想使用标准服务器来服务您的模型,请参阅 TensorFlow Serving 基本教程

本教程使用 TensorFlow 教程中介绍的简单 Softmax 回归模型,用于手写图像(MNIST 数据)分类。如果您不知道 TensorFlow 或 MNIST 是什么,请参阅 MNIST for ML Beginners 教程。

本教程的代码包含两个部分

  • 一个 Python 文件 mnist_saved_model.py,用于训练和导出模型的多个版本。

  • 一个 C++ 文件 main.cc,它是标准 TensorFlow ModelServer,它会发现新导出的模型并运行一个 gRPC 服务来服务它们。

本教程将逐步完成以下任务

  1. 训练和导出 TensorFlow 模型。
  2. 使用 TensorFlow Serving ServerCore 管理模型版本控制。
  3. 使用 SavedModelBundleSourceAdapterConfig 配置批处理。
  4. 使用 TensorFlow Serving ServerCore 服务请求。
  5. 运行和测试服务。

在开始之前,请先 安装 Docker

训练和导出 TensorFlow 模型

首先,如果您还没有这样做,请将此存储库克隆到您的本地机器

git clone https://github.com/tensorflow/serving.git
cd serving

如果导出目录已存在,请清除它

rm -rf /tmp/models

训练(使用 100 次迭代)并导出模型的第一个版本

tools/run_in_docker.sh python tensorflow_serving/example/mnist_saved_model.py \
  --training_iteration=100 --model_version=1 /tmp/mnist

训练(使用 2000 次迭代)并导出模型的第二个版本

tools/run_in_docker.sh python tensorflow_serving/example/mnist_saved_model.py \
  --training_iteration=2000 --model_version=2 /tmp/mnist

如您在 mnist_saved_model.py 中所见,训练和导出与 TensorFlow Serving 基本教程 中的方式相同。为了演示目的,您有意地将第一次运行的训练迭代次数降低并将其导出为 v1,而将第二次运行的训练迭代次数正常化并将其导出为 v2 到同一个父目录 - 因为我们预计由于更密集的训练,后者将实现更好的分类精度。您应该在 /tmp/mnist 目录中看到每次训练运行的训练数据

$ ls /tmp/mnist
1  2

ServerCore

现在想象一下,v1 和 v2 的模型是在运行时动态生成的,因为正在尝试新的算法,或者因为模型正在使用新的数据集进行训练。在生产环境中,您可能希望构建一个可以支持逐步推出(gradual rollout)的服务器,其中 v1 在服务 v1 的同时,可以发现、加载、试验、监控或回滚 v2。或者,您可能希望在启动 v2 之前拆除 v1。TensorFlow Serving 支持这两种选项 - 虽然一种选项有利于在过渡期间保持可用性,但另一种选项有利于最大限度地减少资源使用量(例如 RAM)。

TensorFlow Serving Manager 正是为此而设计的。它处理 TensorFlow 模型的完整生命周期,包括加载、服务和卸载它们,以及版本过渡。在本教程中,您将在 TensorFlow Serving ServerCore 之上构建您的服务器,该服务器在内部包装了一个 AspiredVersionsManager

int main(int argc, char** argv) {
  ...

  ServerCore::Options options;
  options.model_server_config = model_server_config;
  options.servable_state_monitor_creator = &CreateServableStateMonitor;
  options.custom_model_config_loader = &LoadCustomModelConfig;

  ::google::protobuf::Any source_adapter_config;
  SavedModelBundleSourceAdapterConfig
      saved_model_bundle_source_adapter_config;
  source_adapter_config.PackFrom(saved_model_bundle_source_adapter_config);
  (*(*options.platform_config_map.mutable_platform_configs())
      [kTensorFlowModelPlatform].mutable_source_adapter_config()) =
      source_adapter_config;

  std::unique_ptr<ServerCore> core;
  TF_CHECK_OK(ServerCore::Create(options, &core));
  RunServer(port, std::move(core));

  return 0;
}

ServerCore::Create() 接受一个 ServerCore::Options 参数。以下是一些常用的选项

  • **ModelServerConfig** 指定要加载的模型。模型可以通过以下两种方式声明:**model_config_list**,它声明一个静态的模型列表;或者通过**custom_model_config**,它定义一种自定义方式来声明模型列表,该列表可以在运行时更新。
  • **PlatformConfigMap** 将平台名称(例如 **tensorflow**)映射到 **PlatformConfig**,后者用于创建 **SourceAdapter**。**SourceAdapter** 将 **StoragePath**(发现模型版本的位置)适配到模型 **Loader**(从存储路径加载模型版本并向 **Manager** 提供状态转换接口)。如果 **PlatformConfig** 包含 **SavedModelBundleSourceAdapterConfig**,则会创建一个 **SavedModelBundleSourceAdapter**,我们将在后面解释。

**SavedModelBundle** 是 TensorFlow Serving 的一个关键组件。它代表从给定路径加载的 TensorFlow 模型,并提供与 TensorFlow 相同的 **Session::Run** 接口来运行推理。**SavedModelBundleSourceAdapter** 将存储路径适配到 **Loader**,以便 **Manager** 可以管理模型的生命周期。请注意,**SavedModelBundle** 是已弃用的 **SessionBundle** 的继任者。鼓励用户使用 **SavedModelBundle**,因为对 **SessionBundle** 的支持很快就会被移除。

有了这些,**ServerCore** 在内部执行以下操作:

  • 实例化一个 **FileSystemStoragePathSource**,它监控在 **model_config_list** 中声明的模型导出路径。
  • 使用 **PlatformConfigMap** 实例化一个 **SourceAdapter**,该地图包含在 **model_config_list** 中声明的模型平台,并将 **FileSystemStoragePathSource** 连接到它。这样,每当在导出路径下发现新的模型版本时,**SavedModelBundleSourceAdapter** 就会将其适配到 **Loader**。
  • 实例化 **Manager** 的一个特定实现,称为 **AspiredVersionsManager**,它管理由 **SavedModelBundleSourceAdapter** 创建的所有此类 **Loader** 实例。**ServerCore** 通过将调用委托给 **AspiredVersionsManager** 来导出 **Manager** 接口。

每当有新版本可用时,此 **AspiredVersionsManager** 就会加载新版本,并在其默认行为下卸载旧版本。如果您想开始自定义,建议您了解它在内部创建的组件以及如何配置它们。

值得一提的是,TensorFlow Serving 从一开始就被设计得非常灵活和可扩展。您可以构建各种插件来自定义系统行为,同时利用 **ServerCore** 和 **AspiredVersionsManager** 等通用核心组件。例如,您可以构建一个数据源插件来监控云存储而不是本地存储,或者您可以构建一个版本策略插件以不同的方式进行版本转换——实际上,您甚至可以构建一个自定义模型插件来服务非 TensorFlow 模型。这些主题超出了本教程的范围。但是,您可以参考 [自定义源](/tfx/serving/custom_source) 和 [自定义可服务](/tfx/serving/custom_servable) 教程以了解更多信息。

**批处理**

生产环境中我们需要的另一个典型服务器功能是批处理。现代硬件加速器(GPU 等)用于执行机器学习推理,通常在以大批次运行推理请求时才能实现最佳计算效率。

通过在创建 **SavedModelBundleSourceAdapter** 时提供适当的 **SessionBundleConfig**,可以打开批处理。在这种情况下,我们使用几乎默认值设置 **BatchingParameters**。可以通过设置自定义超时、批次大小等值来微调批处理。有关详细信息,请参阅 **BatchingParameters**。

SessionBundleConfig session_bundle_config;
// Batching config
if (enable_batching) {
  BatchingParameters* batching_parameters =
      session_bundle_config.mutable_batching_parameters();
  batching_parameters->mutable_thread_pool_name()->set_value(
      "model_server_batch_threads");
}
*saved_model_bundle_source_adapter_config.mutable_legacy_config() =
    session_bundle_config;

达到完整批次后,推理请求会在内部合并到一个大的请求(张量)中,并调用 **tensorflow::Session::Run()**(这就是 GPU 上实际效率提升的来源)。

**使用 Manager 服务**

如上所述,TensorFlow Serving **Manager** 被设计为一个通用组件,可以处理由任意机器学习系统生成的模型的加载、服务、卸载和版本转换。它的 API 是围绕以下关键概念构建的:

  • **可服务**:可服务是任何可以用于服务客户端请求的不透明对象。可服务的尺寸和粒度是灵活的,因此单个可服务可能包含从查找表的单个分片到单个机器学习模型到模型元组的任何内容。可服务可以是任何类型和接口。

  • **可服务版本**:可服务是版本化的,TensorFlow Serving **Manager** 可以管理一个或多个版本的可服务。版本控制允许同时加载多个版本的可服务,支持逐步推出和实验。

  • **可服务流**:可服务流是可服务版本的序列,版本号递增。

  • **模型**:机器学习模型由一个或多个可服务表示。可服务的示例包括:

    • TensorFlow 会话或围绕它们的包装器,例如 **SavedModelBundle**。
    • 其他类型的机器学习模型。
    • 词汇表查找表。
    • 嵌入查找表。

    复合模型可以表示为多个独立的可服务,也可以表示为单个复合可服务。可服务也可以对应于模型的一部分,例如,大型查找表被分片到许多 **Manager** 实例中。

将所有这些放到本教程的上下文中:

  • TensorFlow 模型由一种可服务表示——**SavedModelBundle**。**SavedModelBundle** 在内部包含一个 **tensorflow:Session**,以及有关什么图加载到会话中以及如何运行它以进行推理的一些元数据。

  • 有一个文件系统目录包含 TensorFlow 导出的流,每个导出都在其自己的子目录中,子目录的名称是版本号。外部目录可以被认为是正在服务的 TensorFlow 模型的可服务流的序列化表示。每个导出对应于可以加载的可服务。

  • **AspiredVersionsManager** 监控导出流,并动态管理所有 **SavedModelBundle** 可服务的生命周期。

**TensorflowPredictImpl::Predict** 然后只需:

  • 从管理器(通过 ServerCore)请求 **SavedModelBundle**。
  • 使用 **通用签名** 将 **PredictRequest** 中的逻辑张量名称映射到实际张量名称,并将值绑定到张量。
  • 运行推理。

**测试和运行服务器**

将导出的第一个版本复制到监控文件夹。

mkdir /tmp/monitored
cp -r /tmp/mnist/1 /tmp/monitored

然后启动服务器。

docker run -p 8500:8500 \
  --mount type=bind,source=/tmp/monitored,target=/models/mnist \
  -t --entrypoint=tensorflow_model_server tensorflow/serving --enable_batching \
  --port=8500 --model_name=mnist --model_base_path=/models/mnist &

服务器将每秒发出日志消息,显示“Aspiring version for servable ...”,这意味着它已找到导出,并且正在跟踪其持续存在。

让我们使用 **--concurrency=10** 运行客户端。这将向服务器发送并发请求,从而触发您的批处理逻辑。

tools/run_in_docker.sh python tensorflow_serving/example/mnist_client.py \
  --num_tests=1000 --server=127.0.0.1:8500 --concurrency=10

这将导致类似于以下内容的输出:

...
Inference error rate: 13.1%

然后我们将导出的第二个版本复制到监控文件夹并重新运行测试。

cp -r /tmp/mnist/2 /tmp/monitored
tools/run_in_docker.sh python tensorflow_serving/example/mnist_client.py \
  --num_tests=1000 --server=127.0.0.1:8500 --concurrency=10

这将导致类似于以下内容的输出:

...
Inference error rate: 9.5%

这确认您的服务器会自动发现新版本并将其用于服务!