PHP 调用 ONNX Runtime C API
解读
在国内高并发、低延迟的 AI 业务场景(如电商推荐、短视频内容审核、金融风控)中,PHP 端往往只负责网关层,真正的推理计算由 C++/Python 服务完成。但受限于“全部业务必须跑在 PHP-FPM 容器内”“不能额外开端口”“延迟预算 < 30 ms”等硬性要求,面试官想确认候选人能否把 ONNX Runtime 直接编译进 PHP 扩展,用 C API 完成模型加载与推理,从而避免跨进程 RPC。题目表面问“怎么调”,实则考察四点:
- 是否了解 ONNX Runtime C API 的内存模型与线程安全规则;
- 能否在 Linux 下用 php-src 的扩展骨架(config.m4、php_*.h)正确封装 C API;
- 是否掌握 PHP 的 zval ⇄ OrtValue 的零拷贝或高效拷贝技巧;
- 对 PHP-FPM 多进程生命周期、共享内存、OPcache 冲突的规避方案。
知识点
- ONNX Runtime C API 核心对象:OrtEnv、OrtSession、OrtMemoryInfo、OrtValue、OrtRunOptions。
- PHP 扩展编译流程:phpize、config.m4、ZEND_BEGIN_ARG_INFO、PHP_FUNCTION、PHP_MINIT_FUNCTION、PHP_MSHUTDOWN_FUNCTION。
- PHP 7/8 类型系统:IS_ARRAY、IS_DOUBLE、IS_LONG、ZVAL_ARR、zend_hash_index_find、add_next_index_double。
- 内存对齐:ONNX Runtime 默认要求 64 字节对齐,PHP 的 emalloc 仅 8 字节对齐,需用 ort_allocator->Alloc 再拷贝回 zval。
- 线程安全:ONNX Runtime 的 OrtEnv 全局唯一,PHP-FPM 多进程模型下须在 MINIT 阶段创建,禁止在 RINIT 重复 ort_create_env。
- 模型缓存:把 .onnx 文件 mmap 到共享内存,结合 SHMOP 或 APCu,避免每个 worker 重复加载。
- 错误处理:OrtGetLastError 信息通过 php_error_docref 抛出 E_WARNING,防止段错误导致 502。
- 性能指标:单次 1×224×224×3 float32 图像推理,在 i7-12700K 上目标 < 6 ms;内存占用增量 < 50 MB。
答案
-
环境准备
- 安装 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
-
编写 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 -
头文件与全局变量
#include "php.h" #include "onnxruntime_c_api.h" static OrtEnv* ort_env = NULL; static OrtSessionOptions* ort_opt = NULL; -
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; } -
核心函数: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); } -
编译与启用
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 -
PHP 业务调用
$input = [1.0, 2.0, 3.0, ..., 150528.0]; // 1×224×224×3 展平 $output = onnxrt_run("/models/resnet50.onnx", $input); var_dump($output); // 概率数组
拓展思考
- 模型热更新:利用 inotify 监听 .onnx 文件,在 RSHUTDOWN 中把旧 session 引用计数归零,再原子替换指针,实现业务无重启升级。
- 批量推理:把 PHP 的二维数组按 row-major 拼成连续 float32 块,一次 OrtRun 处理 N 条样本,降低 30% CPU。
- GPU 支持:在 config.m4 增加 libonnxruntime_providers_cuda.so, OrtSessionOptionsAppendExecutionProvider_CUDA,注意 PHP-FPM 子进程 fork 前必须 cudaSetDevice,否则出现 CUDA context 继承错乱。
- 安全隔离:若 SaaS 平台允许多租户上传模型,需在 OrtCreateSession 前调用 OrtSessionOptionsSetGraphOptimizationLevel(ORT_DISABLE_ALL),防止恶意模型触发 CPU 占用 100% 的常量折叠攻击。
- 性能剖析:开启 ONNX Runtime 的 SessionProfiling,将 .json 结果回传 Prometheus,结合 PHP 的 opcache.enable_cli=0 对比,量化扩展加载耗时与推理耗时,持续优化至 P99 < 20 ms。