SavedModel 加载推理

解读

在国内一线互联网公司的 PHP 后端面试中,面试官提出“SavedModel 加载推理”并不是想让候选人用 PHP 重写 TensorFlow,而是考察以下三点:

  1. 是否知道 SavedModel 是 TensorFlow 的标准导出格式(含 pb 变量资产与签名),以及它通常由 Python 训练产生;
  2. 是否能在 PHP 生态内找到最经济、最稳定的“调用”路径,而不是自己造轮子;
  3. 是否具备高并发场景下的工程化思维:异步、队列、缓存、超时、降级、监控。

因此,题目本质是“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 完成推理”,步骤如下:

  1. 模型准备 训练完成后使用 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

  2. 服务发现 在 Kubernetes 环境,通过 ClusterIP Service + Headless Service 做服务发现;裸机环境用 Consul 或公司自研名字服务,域名如 tfs-my-model.internal,TTL 3s。

  3. 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);
  1. 缓存与异步 对读多写少场景,用 Redis + 特征哈希缓存结果,TTL 300s;写操作或实时要求高的,把请求写入 Kafka,由 Swoole 多进程 Consumer 批量调 TF Serving,回写结果。

  2. 监控 在 predict() 方法里埋点:Prometheus Counter(tfs_request_total)和 Histogram(tfs_latency_seconds),配合 Grafana 看板;当 P99 延迟 > 500ms 或错误率 > 1% 时触发告警,自动扩容 TF Serving 副本。

拓展思考

  1. 如果公司禁止跨语言 RPC,是否可以用 PHP 扩展 tensorflow/php 直接加载 SavedModel?
    答:扩展已两年无人维护,仅支持 TF 2.4,且 fpm 下每次请求重新映射内存,并发高时会 OOM;生产环境不建议,只能做离线 CLI 脚本。

  2. 模型热更新时如何保证零丢包?
    TF Serving 支持 A/B 版本同时加载,通过 --enable_model_warmup 与 --max_num_load_retries 控制;PHP 侧在路由层根据 header 灰度 5% 流量到新版本,对比效果后再全量切换。

  3. 输入是文本序列,需先走分词怎么办?
    把分词模型也导出成 SavedModel,统一走 TF Serving;PHP 只负责转发原文本,避免在 PHP 里维护词表,减少内存碎片。

  4. 高并发下 batch 太小导致 GPU 利用率低?
    在 Swoole 进程内做动态 batch:收集 5ms 内的请求,拼成 32 条再调 TF Serving,延迟增加 < 10ms,吞吐提升 3 倍。

  5. 如果模型必须私有化到边缘节点,而边缘只有 CPU?
    用 TF Lite 转换 SavedModel,再通过 tflite_runtime 的 C API 写 PHP 扩展,内存占用 < 50MB,单核 QPS 仍可达 200+,适合门店盒子、本地广告推荐等场景。