PHP 调用 ONNX Runtime C API

解读

在国内高并发、低延迟的 AI 业务场景(如电商推荐、短视频内容审核、金融风控)中,PHP 端往往只负责网关层,真正的推理计算由 C++/Python 服务完成。但受限于“全部业务必须跑在 PHP-FPM 容器内”“不能额外开端口”“延迟预算 < 30 ms”等硬性要求,面试官想确认候选人能否把 ONNX Runtime 直接编译进 PHP 扩展,用 C API 完成模型加载与推理,从而避免跨进程 RPC。题目表面问“怎么调”,实则考察四点:

  1. 是否了解 ONNX Runtime C API 的内存模型与线程安全规则;
  2. 能否在 Linux 下用 php-src 的扩展骨架(config.m4、php_*.h)正确封装 C API;
  3. 是否掌握 PHP 的 zval ⇄ OrtValue 的零拷贝或高效拷贝技巧;
  4. 对 PHP-FPM 多进程生命周期、共享内存、OPcache 冲突的规避方案。

知识点

  1. ONNX Runtime C API 核心对象:OrtEnv、OrtSession、OrtMemoryInfo、OrtValue、OrtRunOptions。
  2. PHP 扩展编译流程:phpize、config.m4、ZEND_BEGIN_ARG_INFO、PHP_FUNCTION、PHP_MINIT_FUNCTION、PHP_MSHUTDOWN_FUNCTION。
  3. PHP 7/8 类型系统:IS_ARRAY、IS_DOUBLE、IS_LONG、ZVAL_ARR、zend_hash_index_find、add_next_index_double。
  4. 内存对齐:ONNX Runtime 默认要求 64 字节对齐,PHP 的 emalloc 仅 8 字节对齐,需用 ort_allocator->Alloc 再拷贝回 zval。
  5. 线程安全:ONNX Runtime 的 OrtEnv 全局唯一,PHP-FPM 多进程模型下须在 MINIT 阶段创建,禁止在 RINIT 重复 ort_create_env。
  6. 模型缓存:把 .onnx 文件 mmap 到共享内存,结合 SHMOP 或 APCu,避免每个 worker 重复加载。
  7. 错误处理:OrtGetLastError 信息通过 php_error_docref 抛出 E_WARNING,防止段错误导致 502。
  8. 性能指标:单次 1×224×224×3 float32 图像推理,在 i7-12700K 上目标 < 6 ms;内存占用增量 < 50 MB。

答案

  1. 环境准备

    • 安装 onnxruntime-linux-x64-1.17.0.tgz,把头文件与 libonnxruntime.so.1.17.0 放到 /usr/local/onnxruntime;
    • 下载与 PHP 版本一致的 php-src,进入 ext 目录,执行
      ./ext_skel.php --ext onnxrt --dir ./onnxrt
      
  2. 编写 config.m4

    PHP_ARG_WITH(onnxrt, for onnxrt support,
    [  --with-onnxrt[=DIR]      Include ONNX Runtime support])
    
    if test "$PHP_ONNXRT" != "no"; then
      ONNXRT_DIR=$PHP_ONNXRT
      PHP_ADD_INCLUDE($ONNXRT_DIR/include)
      PHP_ADD_LIBRARY_WITH_PATH(onnxruntime, $ONNXRT_DIR/lib)
      PHP_SUBST(ONNXRT_SHARED_LIBADD)
      PHP_NEW_EXTENSION(onnxrt, onnxrt.c, $ext_shared)
    fi
    
  3. 头文件与全局变量

    #include "php.h"
    #include "onnxruntime_c_api.h"
    static OrtEnv* ort_env = NULL;
    static OrtSessionOptions* ort_opt = NULL;
    
  4. MINIT 与 MSHUTDOWN

    PHP_MINIT_FUNCTION(onnxrt)
    {
        OrtStatus* s = OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, "php", &ort_env);
        if (s) return FAILURE;
        ort_opt = OrtCreateSessionOptions();
        OrtSetIntraOpNumThreads(ort_opt, 1);
        return SUCCESS;
    }
    
    PHP_MSHUTDOWN_FUNCTION(onnxrt)
    {
        OrtReleaseSessionOptions(ort_opt);
        OrtReleaseEnv(ort_env);
        return SUCCESS;
    }
    
  5. 核心函数:onnxrt_run

    PHP_FUNCTION(onnxrt_run)
    {
        char* model_path;
        size_t model_path_len;
        zval* input_zv;
    
        ZEND_PARSE_PARAMETERS_START(2, 2)
            Z_PARAM_STRING(model_path, model_path_len)
            Z_PARAM_ARRAY(input_zv)
        ZEND_PARSE_PARAMETERS_END();
    
        OrtSession* session;
        OrtStatus* s = OrtCreateSession(ort_env, model_path, ort_opt, &session);
        if (s) {
            php_error_docref(NULL, E_WARNING, "Load model failed: %s", OrtGetErrorMessage(s));
            RETURN_FALSE;
        }
    
        /* 把 PHP 数组转成 OrtValue */
        OrtMemoryInfo* mem_info;
        OrtCreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &mem_info);
        size_t len = zend_hash_num_elements(Z_ARRVAL_P(input_zv));
        float* data = (float*)OrtAllocatorAlloc(ort_allocator, len * sizeof(float));
        zval* val;
        int i = 0;
        ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(input_zv), val) {
            data[i++] = (float)zval_get_double(val);
        } ZEND_HASH_FOREACH_END();
    
        int64_t shape[] = {1, (int64_t)len};
        OrtValue* input_tensor;
        OrtCreateTensorWithDataAsOrtValue(mem_info, data, len * sizeof(float), shape, 2, ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &input_tensor);
    
        const char* input_names[] = {"input"};
        const char* output_names[] = {"output"};
        OrtValue* output_tensor;
        OrtRun(session, NULL, input_names, &input_tensor, 1, output_names, 1, &output_tensor);
    
        /* 把 OrtValue 转回 PHP 数组 */
        float* out_data;
        OrtGetTensorMutableData(output_tensor, (void**)&out_data);
        size_t out_len;
        OrtGetTensorShapeElementCount(output_tensor, &out_len);
        array_init(return_value);
        for (i = 0; i < out_len; i++) {
            add_next_index_double(return_value, (double)out_data[i]);
        }
    
        OrtReleaseValue(output_tensor);
        OrtReleaseValue(input_tensor);
        OrtAllocatorFree(ort_allocator, data);
        OrtReleaseMemoryInfo(mem_info);
        OrtReleaseSession(session);
    }
    
  6. 编译与启用

    phpize
    ./configure --with-onnxrt=/usr/local/onnxruntime
    make && make install
    echo "extension=onnxrt.so" > /etc/php/8.2/fpm/conf.d/onnxrt.ini
    systemctl reload php8.2-fpm
    
  7. PHP 业务调用

    $input = [1.0, 2.0, 3.0, ..., 150528.0]; // 1×224×224×3 展平
    $output = onnxrt_run("/models/resnet50.onnx", $input);
    var_dump($output); // 概率数组
    

拓展思考

  1. 模型热更新:利用 inotify 监听 .onnx 文件,在 RSHUTDOWN 中把旧 session 引用计数归零,再原子替换指针,实现业务无重启升级。
  2. 批量推理:把 PHP 的二维数组按 row-major 拼成连续 float32 块,一次 OrtRun 处理 N 条样本,降低 30% CPU。
  3. GPU 支持:在 config.m4 增加 libonnxruntime_providers_cuda.so, OrtSessionOptionsAppendExecutionProvider_CUDA,注意 PHP-FPM 子进程 fork 前必须 cudaSetDevice,否则出现 CUDA context 继承错乱。
  4. 安全隔离:若 SaaS 平台允许多租户上传模型,需在 OrtCreateSession 前调用 OrtSessionOptionsSetGraphOptimizationLevel(ORT_DISABLE_ALL),防止恶意模型触发 CPU 占用 100% 的常量折叠攻击。
  5. 性能剖析:开启 ONNX Runtime 的 SessionProfiling,将 .json 结果回传 Prometheus,结合 PHP 的 opcache.enable_cli=0 对比,量化扩展加载耗时与推理耗时,持续优化至 P99 < 20 ms。