SavedModel 加载推理
解读
在国内一线互联网公司的 PHP 后端面试中,面试官提出“SavedModel 加载推理”并不是想让候选人用 PHP 重写 TensorFlow,而是考察以下三点:
- 是否知道 SavedModel 是 TensorFlow 的标准导出格式(含 pb 变量资产与签名),以及它通常由 Python 训练产生;
- 是否能在 PHP 生态内找到最经济、最稳定的“调用”路径,而不是自己造轮子;
- 是否具备高并发场景下的工程化思维:异步、队列、缓存、超时、降级、监控。
因此,题目本质是“PHP 如何可靠地消费 TensorFlow SavedModel 完成在线推理”,既考模型部署常识,也考 PHP 与异构系统的集成能力。
知识点
- SavedModel 目录结构:saved_model.pb + variables/ + assets/
- SignatureDef:定义输入张量名、输出张量名、方法名(默认 serving_default)
- TensorFlow Serving(TF Serving)RESTful 与 gRPC 接口规范
- PHP 7.4+ 的 JSON 精度问题(float32 转 JSON 后精度丢失)及解决方案
- Guzzle 异步池、Swoole\Coroutine\Http\Client 并发模型
- 本地缓存(OPcache/APCu)与 Redis 缓存(特征→结果)的命中率计算
- 降级策略:返回默认分值、走规则模型、写入延迟队列
- 监控指标:QPS、P99 延迟、TF Serving 连接池健康度、缓存命中率
- 安全:内网 DNS、TLS 双向认证、签名防重放、输入 Shape 校验防 OOM
答案
线上标准做法是“PHP 不直接加载 SavedModel,而是通过 TF Serving 完成推理”,步骤如下:
-
模型准备 训练完成后使用 tf.saved_model.save(model, 'export/1'),生成版本号目录;把 export/ 挂载到 TF Serving 的 --model_base_path,启动 docker 镜像: docker run -d --name tfs
-p 8501:8501 -p 8500:8500
-v /data/export:/models/my_model
-e MODEL_NAME=my_model
tensorflow/serving:2.14.0 -
服务发现 在 Kubernetes 环境,通过 ClusterIP Service + Headless Service 做服务发现;裸机环境用 Consul 或公司自研名字服务,域名如 tfs-my-model.internal,TTL 3s。
-
PHP 客户端封装(以 Guzzle 为例)
class TfsClient
{
private $client;
private $url;
public function __construct(string $host, int $port, string $model, int $version = null)
{
$this->url = "http://{$host}:{$port}/v1/models/{$model}" . ($version ? "/versions/{$version}" : "") . ":predict";
$this->client = new \GuzzleHttp\Client([
'timeout' => 0.8,
'connect_timeout' => 0.2,
'headers' => ['Content-Type' => 'application/json'],
]);
}
public function predict(array $instances): array
{
$body = json_encode(['instances' => $instances], JSON_PRESERVE_ZERO_FRACTION);
try {
$resp = $this->client->post($this->url, ['body' => $body]);
$data = json_decode($resp->getBody(), true);
return $data['predictions'] ?? [];
} catch (\GuzzleHttp\Exception\GuzzleException $e) {
// 统一降级
return [['score' => 0.5]];
}
}
}
调用端:
$client = new TfsClient('tfs-my-model.internal', 8501, 'my_model');
$batch = [['input' => [0.1, 0.2, 0.3]], ['input' => [0.4, 0.5, 0.6]]];
$result = $client->predict($batch);
-
缓存与异步 对读多写少场景,用 Redis + 特征哈希缓存结果,TTL 300s;写操作或实时要求高的,把请求写入 Kafka,由 Swoole 多进程 Consumer 批量调 TF Serving,回写结果。
-
监控 在 predict() 方法里埋点:Prometheus Counter(tfs_request_total)和 Histogram(tfs_latency_seconds),配合 Grafana 看板;当 P99 延迟 > 500ms 或错误率 > 1% 时触发告警,自动扩容 TF Serving 副本。
拓展思考
-
如果公司禁止跨语言 RPC,是否可以用 PHP 扩展 tensorflow/php 直接加载 SavedModel?
答:扩展已两年无人维护,仅支持 TF 2.4,且 fpm 下每次请求重新映射内存,并发高时会 OOM;生产环境不建议,只能做离线 CLI 脚本。 -
模型热更新时如何保证零丢包?
TF Serving 支持 A/B 版本同时加载,通过 --enable_model_warmup 与 --max_num_load_retries 控制;PHP 侧在路由层根据 header 灰度 5% 流量到新版本,对比效果后再全量切换。 -
输入是文本序列,需先走分词怎么办?
把分词模型也导出成 SavedModel,统一走 TF Serving;PHP 只负责转发原文本,避免在 PHP 里维护词表,减少内存碎片。 -
高并发下 batch 太小导致 GPU 利用率低?
在 Swoole 进程内做动态 batch:收集 5ms 内的请求,拼成 32 条再调 TF Serving,延迟增加 < 10ms,吞吐提升 3 倍。 -
如果模型必须私有化到边缘节点,而边缘只有 CPU?
用 TF Lite 转换 SavedModel,再通过 tflite_runtime 的 C API 写 PHP 扩展,内存占用 < 50MB,单核 QPS 仍可达 200+,适合门店盒子、本地广告推荐等场景。