PHP中的AI推理集成:利用FFI调用ONNX Runtime或TensorFlow Lite进行端侧推理
各位开发者,大家好。今天我们要探讨的是如何在PHP中集成AI推理能力,特别是利用FFI(Foreign Function Interface)调用ONNX Runtime或TensorFlow Lite,实现在PHP应用中进行端侧推理。这对于需要高性能、低延迟,或者离线推理的应用场景至关重要。
1. 为什么要在PHP中进行AI推理?
PHP通常被认为是一种Web开发的脚本语言,与Python等AI领域常用的语言相比,似乎天然存在隔阂。但实际上,在以下场景中,在PHP中集成AI推理是有价值的:
- 现有PHP项目集成AI功能: 如果你有一个成熟的PHP项目,希望增加图像识别、自然语言处理等AI功能,直接在PHP中集成推理能力可以避免复杂的跨语言调用和数据传输。
- 高并发、低延迟需求: 对于一些需要处理大量并发请求,并且对响应时间有严格要求的应用,例如实时推荐、反欺诈系统等,直接在PHP中进行推理可以减少延迟,提高性能。
- 端侧部署: ONNX Runtime和TensorFlow Lite都支持端侧部署,可以在资源受限的设备上运行。如果你的PHP应用需要部署在边缘设备上,例如IoT设备,那么使用FFI调用它们进行推理是一个不错的选择。
- 避免Python依赖: 某些场景下,引入Python环境可能会带来部署和维护的复杂性。如果你的团队更熟悉PHP,或者希望避免Python的依赖,直接在PHP中进行推理可以简化部署流程。
2. FFI:连接PHP与C/C++的桥梁
FFI是PHP 7.4引入的一项强大功能,它允许PHP代码直接调用C/C++动态链接库(DLL或SO)中的函数,而无需编写扩展。这为PHP集成底层库提供了极大的灵活性。
-
FFI的优势:
- 高性能: 直接调用C/C++代码,避免了跨语言调用的开销,性能接近原生C/C++代码。
- 灵活性: 可以调用任何C/C++库,只要有头文件和动态链接库。
- 易用性: 使用FFI只需要简单的PHP代码即可完成调用,无需编写复杂的扩展。
-
FFI的局限性:
- 安全性: FFI直接操作内存,如果使用不当可能会导致安全问题。需要仔细检查输入和输出,避免内存泄漏和缓冲区溢出。
- 兼容性: FFI需要在PHP 7.4及以上版本才能使用。
- 学习成本: 需要了解C/C++的类型系统和内存管理。
3. ONNX Runtime和TensorFlow Lite:轻量级推理引擎
ONNX Runtime和TensorFlow Lite是两个流行的轻量级推理引擎,它们都旨在提供高性能、低延迟的推理能力,并且都支持多种硬件平台。
-
ONNX Runtime:
- 跨平台: 支持Windows、Linux、macOS、Android、iOS等多种平台。
- 多语言: 提供C、C++、Python、Java、C#等多种语言的API。
- 硬件加速: 支持CPU、GPU、FPGA等多种硬件加速。
- 模型格式: 支持ONNX(Open Neural Network Exchange)模型格式,可以从PyTorch、TensorFlow等框架导出模型。
-
TensorFlow Lite:
- 端侧优化: 专门为端侧设备优化,例如手机、嵌入式设备等。
- 硬件加速: 支持CPU、GPU、Edge TPU等多种硬件加速。
- 模型格式: 支持TensorFlow Lite模型格式(.tflite)。
4. 利用FFI调用ONNX Runtime进行推理
下面我们将演示如何使用FFI调用ONNX Runtime进行推理。
步骤1:安装ONNX Runtime C API
首先,你需要安装ONNX Runtime C API。你可以从ONNX Runtime的官方网站下载预编译的库,或者自己编译。
假设你已经将ONNX Runtime的头文件(例如onnxruntime_c_api.h)放在/usr/local/include/onnxruntime目录下,将动态链接库(例如libonnxruntime.so)放在/usr/local/lib目录下。
步骤2:编写PHP代码
<?php
// 定义ONNX Runtime C API的函数签名
$ffi = FFI::cdef(
"
typedef struct OrtEnv OrtEnv;
typedef struct OrtSessionOptions OrtSessionOptions;
typedef struct OrtSession OrtSession;
typedef struct OrtValue OrtValue;
typedef struct OrtAllocator OrtAllocator;
typedef struct OrtMemoryInfo OrtMemoryInfo;
typedef struct OrtTensorTypeAndShapeInfo OrtTensorTypeAndShapeInfo;
typedef enum OrtDataType {
ORT_INVALID = 0,
ORT_FLOAT = 1,
ORT_UINT8 = 2,
ORT_INT8 = 3,
ORT_UINT16 = 4,
ORT_INT16 = 5,
ORT_INT32 = 6,
ORT_INT64 = 7,
ORT_STRING = 8,
ORT_BOOL = 9,
ORT_FLOAT16 = 10,
ORT_DOUBLE = 11,
ORT_UINT32 = 12,
ORT_UINT64 = 13,
ORT_COMPLEX64 = 14,
ORT_COMPLEX128 = 15,
ORT_BFLOAT16 = 16
} OrtDataType;
typedef struct OrtStatus OrtStatus;
const char* OrtGetErrorMessage(const OrtStatus* status);
OrtStatus* OrtCreateEnv(int log_severity_level, const char* log_id, OrtEnv** out);
OrtStatus* OrtCreateSessionOptions(OrtSessionOptions** options);
OrtStatus* OrtCreateSession(const OrtEnv* env, const char* model_path, const OrtSessionOptions* options, OrtSession** out);
OrtStatus* OrtCreateCpuMemoryInfo(int are_arena_segments, int ort_mem_type, OrtMemoryInfo** out);
OrtStatus* OrtCreateTensorWithDataAsOrtValue(const OrtMemoryInfo* memory_info, void* data, size_t data_len, const int64_t* shape, size_t shape_len, OrtDataType type, OrtValue** out);
OrtStatus* OrtRun(const OrtSession* session, const char* const* input_names, const OrtValue* const* input_values, size_t input_count, const char* const* output_names, size_t output_count, OrtValue** output_values);
OrtStatus* OrtGetTensorMutableData(OrtValue* value, void** out);
OrtStatus* OrtGetTensorTypeAndShape(const OrtValue* value, OrtTensorTypeAndShapeInfo** out);
OrtStatus* OrtGetTensorShapeElementCount(const OrtTensorTypeAndShapeInfo* info, size_t* out);
OrtStatus* OrtGetTensorElementType(const OrtTensorTypeAndShapeInfo* info, OrtDataType* out);
void OrtReleaseEnv(OrtEnv* env);
void OrtReleaseSessionOptions(OrtSessionOptions* options);
void OrtReleaseSession(OrtSession* session);
void OrtReleaseValue(OrtValue* value);
void OrtReleaseMemoryInfo(OrtMemoryInfo* memory_info);
void OrtReleaseTensorTypeAndShapeInfo(OrtTensorTypeAndShapeInfo* info);
",
'/usr/local/lib/libonnxruntime.so' // ONNX Runtime 动态链接库的路径
);
// 加载ONNX模型
$model_path = './model.onnx'; // ONNX模型的路径
// 创建ONNX Runtime环境
$env = FFI::addr(FFI::new("OrtEnv*"));
$status = $ffi->OrtCreateEnv(0, "php-inference", $env);
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error creating environment: " . FFI::string($error_message));
exit(1);
}
$env = $env->deref();
// 创建SessionOptions
$session_options = FFI::addr(FFI::new("OrtSessionOptions*"));
$status = $ffi->OrtCreateSessionOptions($session_options);
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error creating session options: " . FFI::string($error_message));
exit(1);
}
$session_options = $session_options->deref();
// 创建Session
$session = FFI::addr(FFI::new("OrtSession*"));
$status = $ffi->OrtCreateSession($env, $model_path, $session_options, $session);
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error creating session: " . FFI::string($error_message));
exit(1);
}
$session = $session->deref();
// 创建MemoryInfo
$memory_info = FFI::addr(FFI::new("OrtMemoryInfo*"));
$status = $ffi->OrtCreateCpuMemoryInfo(0, 0, $memory_info);
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error creating memory info: " . FFI::string($error_message));
exit(1);
}
$memory_info = $memory_info->deref();
// 准备输入数据
$input_name = "input"; // 输入张量的名称
$input_shape = [1, 3, 224, 224]; // 输入张量的形状
$input_data = array_fill(0, array_product($input_shape), 0.0); // 输入数据,这里填充0
$input_data_size = count($input_data);
$input_data_ptr = FFI::new("float[" . $input_data_size . "]");
FFI::memcpy($input_data_ptr, $input_data, FFI::sizeof($input_data_ptr));
// 创建输入Tensor
$input_tensor = FFI::addr(FFI::new("OrtValue*"));
$shape_c = FFI::new("int64_t[" . count($input_shape) . "]", $input_shape);
$status = $ffi->OrtCreateTensorWithDataAsOrtValue(
$memory_info,
FFI::addr($input_data_ptr),
FFI::sizeof($input_data_ptr),
$shape_c,
count($input_shape),
$ffi::ORT_FLOAT,
$input_tensor
);
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error creating input tensor: " . FFI::string($error_message));
exit(1);
}
$input_tensor = $input_tensor->deref();
// 准备输出
$output_name = "output"; // 输出张量的名称
$output_tensors = FFI::new("OrtValue*[1]");
$output_names_c = FFI::new("char*[" . 1 . "]");
$output_names_c[0] = FFI::new("char[" . strlen($output_name) + 1 . "]", false);
FFI::memcpy($output_names_c[0], $output_name, strlen($output_name));
// 运行推理
$input_names_c = FFI::new("char*[" . 1 . "]");
$input_names_c[0] = FFI::new("char[" . strlen($input_name) + 1 . "]", false);
FFI::memcpy($input_names_c[0], $input_name, strlen($input_name));
$input_values = FFI::new("OrtValue*[1]");
$input_values[0] = $input_tensor;
$status = $ffi->OrtRun(
$session,
FFI::addr($input_names_c),
FFI::addr($input_values),
1,
FFI::addr($output_names_c),
1,
$output_tensors
);
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error running inference: " . FFI::string($error_message));
exit(1);
}
// 获取输出数据
$output_tensor = $output_tensors[0];
$tensor_info = FFI::addr(FFI::new("OrtTensorTypeAndShapeInfo*"));
$status = $ffi->OrtGetTensorTypeAndShape($output_tensor, $tensor_info);
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error getting tensor info: " . FFI::string($error_message));
exit(1);
}
$tensor_info = $tensor_info->deref();
$num_elements = FFI::new("size_t");
$status = $ffi->OrtGetTensorShapeElementCount($tensor_info, FFI::addr($num_elements));
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error getting tensor element count: " . FFI::string($error_message));
exit(1);
}
$num_elements = $num_elements->deref();
$data_type = FFI::new("OrtDataType");
$status = $ffi->OrtGetTensorElementType($tensor_info, FFI::addr($data_type));
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error getting tensor data type: " . FFI::string($error_message));
exit(1);
}
$data_type = $data_type->deref();
$output_data_ptr = FFI::addr(FFI::new("void*"));
$status = $ffi->OrtGetTensorMutableData($output_tensor, $output_data_ptr);
if ($status !== null) {
$error_message = $ffi->OrtGetErrorMessage($status);
error_log("Error getting tensor data: " . FFI::string($error_message));
exit(1);
}
$output_data_ptr = $output_data_ptr->deref();
//根据data_type和num_elements读取数据
$output_data = [];
if($data_type == $ffi::ORT_FLOAT){
$output_data_float = FFI::cast("float*", $output_data_ptr);
for($i=0; $i<$num_elements; $i++){
$output_data[] = $output_data_float[$i];
}
}
// 输出结果
print_r($output_data);
// 释放资源
$ffi->OrtReleaseValue($input_tensor);
$ffi->OrtReleaseValue($output_tensor);
$ffi->OrtReleaseMemoryInfo($memory_info);
$ffi->OrtReleaseSession($session);
$ffi->OrtReleaseSessionOptions($session_options);
$ffi->OrtReleaseEnv($env);
?>
步骤3:运行PHP代码
运行PHP代码,你将看到ONNX模型的推理结果。
5. 利用FFI调用TensorFlow Lite进行推理
与ONNX Runtime类似,你也可以使用FFI调用TensorFlow Lite进行推理。
步骤1:安装TensorFlow Lite C API
你需要下载TensorFlow Lite的C API。你可以从TensorFlow Lite的官方网站下载预编译的库,或者自己编译。
假设你已经将TensorFlow Lite的头文件(例如tensorflow/lite/c/c_api.h)放在/usr/local/include/tensorflow/lite/目录下,将动态链接库(例如libtensorflowlite_c.so)放在/usr/local/lib目录下。
步骤2:编写PHP代码
<?php
// 定义TensorFlow Lite C API的函数签名
$ffi = FFI::cdef(
"
typedef struct TfLiteModel TfLiteModel;
typedef struct TfLiteInterpreterOptions TfLiteInterpreterOptions;
typedef struct TfLiteInterpreter TfLiteInterpreter;
typedef struct TfLiteTensor TfLiteTensor;
typedef enum TfLiteType {
kTfLiteNoType = 0,
kTfLiteFloat32 = 1,
kTfLiteInt32 = 2,
kTfLiteUInt8 = 3,
kTfLiteInt64 = 4,
kTfLiteString = 5,
kTfLiteBool = 6,
kTfLiteInt16 = 7,
kTfLiteComplex64 = 8,
kTfLiteInt8 = 9,
kTfLiteFloat16 = 10,
kTfLiteFloat64 = 11,
kTfLiteComplex128 = 12,
kTfLiteUInt16 = 13,
kTfLiteUInt32 = 14,
kTfLiteUInt64 = 15,
} TfLiteType;
TfLiteModel* TfLiteModelCreateFromFile(const char* model_path);
void TfLiteModelDelete(TfLiteModel* model);
TfLiteInterpreterOptions* TfLiteInterpreterOptionsCreate();
void TfLiteInterpreterOptionsDelete(TfLiteInterpreterOptions* options);
void TfLiteInterpreterOptionsSetNumThreads(TfLiteInterpreterOptions* options, int num_threads);
TfLiteInterpreter* TfLiteInterpreterCreate(const TfLiteModel* model, const TfLiteInterpreterOptions* options);
void TfLiteInterpreterDelete(TfLiteInterpreter* interpreter);
int TfLiteInterpreterAllocateTensors(TfLiteInterpreter* interpreter);
int TfLiteInterpreterInvoke(TfLiteInterpreter* interpreter);
int TfLiteInterpreterGetInputTensorCount(const TfLiteInterpreter* interpreter);
TfLiteTensor* TfLiteInterpreterGetInputTensor(const TfLiteInterpreter* interpreter, int input_index);
int TfLiteInterpreterGetOutputTensorCount(const TfLiteInterpreter* interpreter);
TfLiteTensor* TfLiteInterpreterGetOutputTensor(const TfLiteInterpreter* interpreter, int output_index);
void* TfLiteTensorData(const TfLiteTensor* tensor);
TfLiteType TfLiteTensorType(const TfLiteTensor* tensor);
const int* TfLiteTensorDims(const TfLiteTensor* tensor);
int TfLiteTensorNumDims(const TfLiteTensor* tensor);
size_t TfLiteTensorByteSize(const TfLiteTensor* tensor);
",
'/usr/local/lib/libtensorflowlite_c.so' // TensorFlow Lite 动态链接库的路径
);
// 加载TensorFlow Lite模型
$model_path = './model.tflite'; // TensorFlow Lite模型的路径
$model = $ffi->TfLiteModelCreateFromFile($model_path);
if ($model === null) {
error_log("Error loading model: " . $model_path);
exit(1);
}
// 创建InterpreterOptions
$options = $ffi->TfLiteInterpreterOptionsCreate();
if ($options === null) {
error_log("Error creating interpreter options");
exit(1);
}
// 设置线程数 (可选)
$ffi->TfLiteInterpreterOptionsSetNumThreads($options, 4); // 使用4个线程
// 创建Interpreter
$interpreter = $ffi->TfLiteInterpreterCreate($model, $options);
if ($interpreter === null) {
error_log("Error creating interpreter");
exit(1);
}
// 分配张量
if ($ffi->TfLiteInterpreterAllocateTensors($interpreter) != 0) {
error_log("Error allocating tensors");
exit(1);
}
// 获取输入张量
$input_count = $ffi->TfLiteInterpreterGetInputTensorCount($interpreter);
if ($input_count != 1) {
error_log("Expected 1 input tensor, but got " . $input_count);
exit(1);
}
$input_tensor = $ffi->TfLiteInterpreterGetInputTensor($interpreter, 0);
// 获取输入张量的形状
$num_dims = $ffi->TfLiteTensorNumDims($input_tensor);
$dims = $ffi->TfLiteTensorDims($input_tensor);
$input_shape = [];
for ($i = 0; $i < $num_dims; $i++) {
$input_shape[] = $dims[$i];
}
// 准备输入数据
$input_data = array_fill(0, array_product($input_shape), 0.0); // 输入数据,这里填充0
$input_data_size = count($input_data);
$input_data_ptr = $ffi->TfLiteTensorData($input_tensor);
// 将输入数据写入输入张量
FFI::memcpy($input_data_ptr, $input_data, $ffi->TfLiteTensorByteSize($input_tensor));
// 运行推理
if ($ffi->TfLiteInterpreterInvoke($interpreter) != 0) {
error_log("Error invoking interpreter");
exit(1);
}
// 获取输出张量
$output_count = $ffi->TfLiteInterpreterGetOutputTensorCount($interpreter);
if ($output_count != 1) {
error_log("Expected 1 output tensor, but got " . $output_count);
exit(1);
}
$output_tensor = $ffi->TfLiteInterpreterGetOutputTensor($interpreter, 0);
// 获取输出数据
$output_data_ptr = $ffi->TfLiteTensorData($output_tensor);
$output_type = $ffi->TfLiteTensorType($output_tensor);
$output_byte_size = $ffi->TfLiteTensorByteSize($output_tensor);
$num_dims = $ffi->TfLiteTensorNumDims($output_tensor);
$dims = $ffi->TfLiteTensorDims($output_tensor);
$output_shape = [];
for ($i = 0; $i < $num_dims; $i++) {
$output_shape[] = $dims[$i];
}
$output_element_count = array_product($output_shape);
//根据output_type读取数据
$output_data = [];
if($output_type == $ffi::kTfLiteFloat32){
$output_data_float = FFI::cast("float*", $output_data_ptr);
for($i=0; $i<$output_element_count; $i++){
$output_data[] = $output_data_float[$i];
}
}
// 输出结果
print_r($output_data);
// 释放资源
$ffi->TfLiteInterpreterDelete($interpreter);
$ffi->TfLiteInterpreterOptionsDelete($options);
$ffi->TfLiteModelDelete($model);
?>
步骤3:运行PHP代码
运行PHP代码,你将看到TensorFlow Lite模型的推理结果。
6. 性能优化技巧
- 选择合适的硬件加速: ONNX Runtime和TensorFlow Lite都支持多种硬件加速,例如GPU、FPGA、Edge TPU等。选择合适的硬件加速可以显著提高推理性能。
- 模型量化: 模型量化可以将模型的权重和激活值从浮点数转换为整数,从而减小模型的大小,提高推理速度。
- 线程数: 调整ONNX Runtime和TensorFlow Lite的线程数可以优化CPU的利用率。
- 避免重复加载模型: 尽量避免在每次请求时都加载模型,可以将模型缓存在内存中,以提高性能。
- 预处理和后处理优化: 优化图像预处理和后处理流程可以减少CPU的负担。
7. 安全性注意事项
- 输入验证: 仔细检查输入数据,避免恶意输入导致安全问题。
- 内存管理: 使用FFI时需要手动管理内存,避免内存泄漏和缓冲区溢出。
- 动态链接库安全: 确保使用的动态链接库来自可信的来源,避免恶意代码注入。
8. 总结:PHP集成AI推理大有可为
通过FFI,PHP可以方便地调用ONNX Runtime和TensorFlow Lite等推理引擎,实现在PHP应用中进行高性能、低延迟的AI推理。虽然存在一些挑战,例如安全性问题和学习成本,但只要谨慎使用,就可以为PHP应用带来强大的AI能力。