PHP中的AI推理集成:利用FFI调用ONNX Runtime或TensorFlow Lite进行端侧推理

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能力。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注