PHP FFI调用Python/TensorFlow模型:实现高性能的机器学习推理集成

PHP FFI 调用 Python/TensorFlow 模型:实现高性能的机器学习推理集成

大家好,今天我们来探讨一个非常有趣且实用的主题:如何使用 PHP FFI (Foreign Function Interface) 调用 Python/TensorFlow 模型,以实现高性能的机器学习推理集成。在传统的 Web 应用中,机器学习的集成往往涉及到进程间通信,例如使用消息队列或者 HTTP 请求,这会带来显著的性能开销。PHP FFI 的出现为我们提供了一种更为高效的解决方案,可以直接在 PHP 代码中调用 Python 代码,从而避免了昂贵的进程间通信。

1. 背景与动机

在现代 Web 应用中,机器学习模型被广泛用于各种任务,例如:

  • 推荐系统: 根据用户历史行为推荐商品或内容。
  • 图像识别: 识别图像中的物体或场景。
  • 自然语言处理: 分析文本情感或进行机器翻译。
  • 欺诈检测: 识别潜在的欺诈行为。

传统的 PHP 应用集成机器学习模型的方式通常是:

  1. PHP 应用将数据发送到 Python 服务(例如通过 HTTP 请求)。
  2. Python 服务接收数据,加载模型,进行推理。
  3. Python 服务将结果返回给 PHP 应用(例如通过 HTTP 响应)。

这种方式的缺点是明显的:

  • 性能开销: HTTP 请求的开销较高,包括序列化、网络传输、反序列化等。
  • 部署复杂性: 需要维护独立的 Python 服务。
  • 资源消耗: 两个进程分别占用内存和 CPU 资源。

PHP FFI 允许我们在 PHP 进程中直接调用 Python 代码,避免了上述缺点,从而提高性能并简化部署。

2. PHP FFI 简介

PHP FFI 允许 PHP 代码调用 C 语言函数库。通过 FFI,我们可以将 Python 编译成一个动态链接库(.so 文件),然后在 PHP 中加载并调用其中的函数。

FFI 的优势:

  • 高性能: 直接在 PHP 进程中调用 C 代码,避免了进程间通信的开销。
  • 灵活性: 可以调用任何 C 语言函数库,包括 Python 解释器。
  • 易用性: FFI API 相对简单易懂。

FFI 的局限性:

  • 安全性: 需要谨慎处理内存管理,避免内存泄漏或安全漏洞。
  • 调试难度: 跨语言调试可能比较困难。
  • 复杂性: 需要编写 C 语言包装代码。

3. 环境搭建

在开始之前,我们需要安装必要的软件和扩展:

  1. PHP: 确保 PHP 版本 >= 7.4,推荐使用最新版本。

  2. FFI 扩展: 安装 PHP FFI 扩展。

    pecl install ffi

    然后在 php.ini 文件中启用 FFI 扩展:

    extension=ffi.so
  3. Python: 安装 Python 3.x。

  4. TensorFlow: 安装 TensorFlow 或 TensorFlow Lite。

    pip install tensorflow
  5. NumPy: 安装 NumPy (TensorFlow 依赖)。

    pip install numpy
  6. ctypesgen: 安装 ctypesgen,用于生成 C 语言头文件。

    pip install ctypesgen

4. Python 代码准备

我们需要编写一个 Python 脚本,用于加载 TensorFlow 模型并进行推理。同时,我们需要将推理函数封装成 C 语言可以调用的形式。

# predict.py
import tensorflow as tf
import numpy as np
import ctypes

# 全局变量,用于存储模型
model = None

# 加载模型
def load_model(model_path):
    global model
    model = tf.keras.models.load_model(model_path)
    print("Model loaded successfully!")

# 推理函数
def predict(input_data_ptr, input_shape_ptr, output_ptr):
    global model
    if model is None:
        print("Error: Model not loaded!")
        return -1

    # 从指针读取输入数据
    input_shape = np.ctypeslib.as_array(ctypes.cast(input_shape_ptr, ctypes.POINTER(ctypes.c_int)), shape=(2,))
    input_data = np.ctypeslib.as_array(ctypes.cast(input_data_ptr, ctypes.POINTER(ctypes.c_float)), shape=tuple(input_shape))

    # 进行推理
    prediction = model.predict(input_data)

    # 将结果写入指针
    output_data = prediction.flatten() # flatten to 1D array
    output_shape = output_data.shape[0]
    for i in range(output_shape):
        output_ptr[i] = output_data[i]
    print("Prediction done!")
    return 0

# 暴露给 C 语言的函数
if __name__ == '__main__':
    # Example usage (for testing purposes)
    load_model('your_model.h5') # Replace with your model path

    # Prepare input data (example: 1x28x28 image)
    input_data = np.random.rand(1, 28, 28).astype(np.float32)
    input_shape = np.array(input_data.shape, dtype=np.int32)

    # Allocate memory for output
    output_shape = model.output_shape[1] # number of classes
    output_data = np.zeros(output_shape, dtype=np.float32)

    # Convert to pointers (using ctypes)
    input_data_ptr = input_data.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
    input_shape_ptr = input_shape.ctypes.data_as(ctypes.POINTER(ctypes.c_int))
    output_ptr = output_data.ctypes.data_as(ctypes.POINTER(ctypes.c_float))

    # Call the predict function
    status = predict(input_data_ptr, input_shape_ptr, output_ptr)

    if status == 0:
        print("Prediction results:", output_data)
    else:
        print("Prediction failed.")

代码解释:

  • load_model(model_path): 加载 TensorFlow 模型。
  • predict(input_data_ptr, input_shape_ptr, output_ptr): 接收输入数据指针、输入数据形状指针和输出数据指针,进行推理,并将结果写入输出数据指针。
  • input_data_ptr:指向输入数据的内存地址,数据类型为 float
  • input_shape_ptr:指向输入数据形状的内存地址,数据类型为 int
  • output_ptr:指向输出数据的内存地址,数据类型为 float
  • 使用 ctypes 模块将 NumPy 数组转换为 C 语言指针。
  • model.predict() 进行实际的 TensorFlow 推理。
  • output_data.flatten() 将输出结果展平为一维数组。
  • 函数返回状态码,0 表示成功,-1 表示失败。

注意:

  • your_model.h5 替换为你实际的模型文件路径。
  • 根据你的模型输入和输出形状调整代码。
  • 确保输入数据类型为 float32

5. 生成 C 语言头文件

我们需要使用 ctypesgen 工具根据 Python 代码生成 C 语言头文件。

首先,创建一个名为 predict.h 的文件,并添加以下内容:

#ifndef PREDICT_H
#define PREDICT_H

#ifdef __cplusplus
extern "C" {
#endif

int load_model(const char* model_path);
int predict(float* input_data_ptr, int* input_shape_ptr, float* output_ptr);

#ifdef __cplusplus
}
#endif

#endif

然后,使用 ctypesgen 工具生成 C 语言头文件。 由于我们自己定义了头文件,这一步可以省略。 但是,如果需要更详细的类型信息,可以尝试以下命令(可能需要调整):

ctypesgen -L . -lpredict predict.py -o predict.h

重要: 如果使用 ctypesgen 生成头文件,请仔细检查生成的头文件,确保类型定义正确。如果 ctypesgen 无法正确推断类型,你需要手动修改头文件。

6. 创建动态链接库

我们需要将 Python 代码编译成动态链接库(.so 文件),以便 PHP 可以加载和调用。

创建一个名为 setup.py 的文件,并添加以下内容:

# setup.py
from setuptools import setup
from setuptools import Extension

predict_module = Extension(
    'predict',
    sources=['predict.c'], # We'll create predict.c next
    include_dirs=[],
    library_dirs=[],
    libraries=[],
    extra_compile_args=['-fPIC', '-std=c99'],  # Important for shared library
)

setup(
    name='predict',
    version='1.0',
    description='Python module for TensorFlow prediction',
    ext_modules=[predict_module],
)

接下来,创建一个 predict.c 文件作为桥梁,连接Python代码和C代码。

// predict.c
#include <Python.h>
#include <stdio.h>

// Function to load the model
static PyObject *predict_load_model(PyObject *self, PyObject *args) {
    const char *model_path;
    if (!PyArg_ParseTuple(args, "s", &model_path)) {
        return NULL;
    }

    PyObject *pModule = PyImport_ImportModule("__main__");
    if (pModule == NULL) {
        return NULL;
    }
    PyObject *pFunc = PyObject_GetAttrString(pModule, "load_model");
    if (pFunc == NULL || !PyCallable_Check(pFunc)) {
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
        return NULL;
    }

    PyObject *pArgs = PyTuple_New(1);
    PyObject *pValue = PyUnicode_FromString(model_path);
    if (!pValue) {
        Py_DECREF(pArgs);
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
        return NULL;
    }
    PyTuple_SetItem(pArgs, 0, pValue);

    PyObject *pResult = PyObject_CallObject(pFunc, pArgs);
    Py_DECREF(pArgs);
    Py_XDECREF(pFunc);
    Py_DECREF(pModule);

    if (pResult == NULL) {
        return NULL;
    }

    Py_DECREF(pResult);  // We don't need the result itself.
    Py_RETURN_NONE;
}

// Function to perform the prediction
static PyObject *predict_predict(PyObject *self, PyObject *args) {
    float *input_data_ptr;
    int *input_shape_ptr;
    float *output_ptr;

    if (!PyArg_ParseTuple(args, "O!O!O!", &PyArray_Type, &input_data_ptr, &PyArray_Type, &input_shape_ptr, &PyArray_Type, &output_ptr)) {
    return NULL;
    }

    PyObject *pModule = PyImport_ImportModule("__main__");
    if (pModule == NULL) {
        return NULL;
    }
    PyObject *pFunc = PyObject_GetAttrString(pModule, "predict");
    if (pFunc == NULL || !PyCallable_Check(pFunc)) {
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
        return NULL;
    }

    PyObject *pArgs = PyTuple_New(3);
    PyTuple_SetItem(pArgs, 0, PyLong_FromVoidPtr(input_data_ptr));
    PyTuple_SetItem(pArgs, 1, PyLong_FromVoidPtr(input_shape_ptr));
    PyTuple_SetItem(pArgs, 2, PyLong_FromVoidPtr(output_ptr));

    PyObject *pResult = PyObject_CallObject(pFunc, pArgs);
    Py_DECREF(pArgs);
    Py_XDECREF(pFunc);
    Py_DECREF(pModule);

    if (pResult == NULL) {
        return NULL;
    }

    long status = PyLong_AsLong(pResult);
    Py_DECREF(pResult);

    return PyLong_FromLong(status);
}

static PyMethodDef PredictMethods[] = {
    {"load_model", predict_load_model, METH_VARARGS, "Load TensorFlow model."},
    {"predict", predict_predict, METH_VARARGS, "Perform prediction."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef predictmodule = {
    PyModuleDef_HEAD_INIT,
    "predict",   /* name of module */
    NULL, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    PredictMethods
};

PyMODINIT_FUNC
PyInit_predict(void)
{
    PyObject *m = PyModule_Create(&predictmodule);
    if (m == NULL)
        return NULL;

    // NumPy initialization (VERY IMPORTANT)
    import_array();

    return m;
}

构建 .so 文件:

python3 setup.py build_ext --inplace

这将在当前目录生成一个名为 predict.so 的文件。 确保你的setup.pypredict.c 文件在同一个目录下。

7. PHP 代码实现

现在,我们可以编写 PHP 代码来加载动态链接库并调用其中的函数。

<?php

// 1. 定义 FFI 接口
$ffi = FFI::cdef(
    "
    int load_model(const char* model_path);
    int predict(float* input_data_ptr, int* input_shape_ptr, float* output_ptr);
    ",
    __DIR__ . "/predict.so" // 动态链接库的路径
);

// 2. 加载模型
$model_path = __DIR__ . "/your_model.h5"; // 模型文件路径
$result = $ffi->load_model($model_path);
if ($result != 0) {
    echo "Failed to load model.n";
    exit(1);
}

echo "Model loaded successfully.n";

// 3. 准备输入数据
$input_shape = [1, 28, 28]; // 示例输入形状
$input_data = array_fill(0, $input_shape[0] * $input_shape[1] * $input_shape[2], 0.5); // 示例输入数据,假设所有元素都为 0.5

// 创建 FFI 数组
$input_data_size = $input_shape[0] * $input_shape[1] * $input_shape[2];
$input_shape_size = count($input_shape);

$input_data_ptr = FFI::new("float[$input_data_size]");
$input_shape_ptr = FFI::new("int[$input_shape_size]");
$output_ptr = FFI::new("float[10]"); // 假设输出是 10 个类别的概率

// 复制数据到 FFI 数组
FFI::memcpy($input_data_ptr,  FFI::addr($input_data), FFI::sizeof($input_data_ptr));
FFI::memcpy($input_shape_ptr, FFI::addr($input_shape), FFI::sizeof($input_shape_ptr));

// 4. 进行推理
$result = $ffi->predict($input_data_ptr, $input_shape_ptr, $output_ptr);

if ($result != 0) {
    echo "Prediction failed.n";
    exit(1);
}

echo "Prediction done.n";

// 5. 获取结果
$output = [];
for ($i = 0; $i < 10; $i++) {
    $output[] = $output_ptr[$i];
}

print_r($output);

?>

代码解释:

  1. 使用 FFI::cdef() 定义 FFI 接口,包括 load_model()predict() 函数的签名。
  2. 使用 $ffi->load_model() 加载模型。
  3. 准备输入数据,包括输入数据和输入数据形状。
  4. 使用 FFI::new() 创建 FFI 数组,用于存储输入数据和输出数据。
  5. 使用 FFI::memcpy() 将 PHP 数组复制到 FFI 数组。
  6. 使用 $ffi->predict() 进行推理。
  7. 从 FFI 数组中获取结果。

注意:

  • your_model.h5 替换为你实际的模型文件路径。
  • 根据你的模型输入和输出形状调整代码。
  • 确保 PHP 进程有权访问动态链接库和模型文件。
  • 根据模型的输出调整输出数组的大小 (在这里是10, 假设是10个类别).

8. 性能优化

  • 模型优化: 使用 TensorFlow Lite 等工具对模型进行优化,减小模型大小并提高推理速度。
  • 批量推理: 将多个输入数据打包成一个批次进行推理,可以提高吞吐量。
  • 缓存: 缓存推理结果,避免重复计算。
  • 异步处理: 使用异步任务处理推理请求,避免阻塞主线程。

9. 安全性考虑

  • 输入验证: 对输入数据进行严格的验证,防止恶意数据导致安全漏洞。
  • 内存管理: 谨慎处理内存管理,避免内存泄漏或安全漏洞。
  • 权限控制: 限制 PHP 进程的权限,防止恶意代码执行。

10. 常见问题与解决方案

  • FFI 扩展未安装: 确保 FFI 扩展已安装并启用。
  • 动态链接库加载失败: 确保动态链接库的路径正确,并且 PHP 进程有权访问该文件。
  • 类型不匹配: 确保 PHP 代码中使用的类型与 C 语言代码中定义的类型一致。
  • 内存泄漏: 使用 FFI::free() 释放不再使用的内存。
  • 段错误: 检查指针是否有效,以及内存访问是否越界。

11. 代码示例:完整可运行的例子

为了方便大家理解,这里提供一个完整可运行的例子。 这个例子使用一个简单的线性回归模型。

首先,创建一个简单的线性回归模型:

# linear_model.py
import tensorflow as tf
import numpy as np
import ctypes

# Create a simple linear regression model
model = tf.keras.models.Sequential([
    tf.keras.layers.Dense(units=1, input_shape=[1])
])

model.compile(optimizer='sgd', loss='mean_squared_error')

# Train the model (replace with your actual training data)
xs = np.array([-1.0,  0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)
ys = np.array([-3.0, -1.0, 1.0, 3.0, 5.0, 7.0], dtype=float)
model.fit(xs, ys, epochs=500, verbose=0)

# Save the model
model.save('linear_regression_model.h5')

# Global variable to store the model
loaded_model = None

# Load the model
def load_model(model_path):
    global loaded_model
    loaded_model = tf.keras.models.load_model(model_path)
    print("Model loaded successfully!")

# Prediction function
def predict(input_data_ptr, output_ptr):
    global loaded_model
    if loaded_model is None:
        print("Error: Model not loaded!")
        return -1

    # Read input data from pointer
    input_value = np.ctypeslib.as_array(ctypes.cast(input_data_ptr, ctypes.POINTER(ctypes.c_float)), shape=(1,))[0]

    # Perform prediction
    prediction = loaded_model.predict([input_value])[0][0]

    # Write the result to the output pointer
    output_ptr[0] = prediction

    print("Prediction done!")
    return 0

# Example usage (for testing purposes)
if __name__ == '__main__':
    load_model('linear_regression_model.h5')

    # Prepare input data
    input_value = np.array([10.0], dtype=np.float32)

    # Allocate memory for output
    output_value = np.zeros(1, dtype=np.float32)

    # Convert to pointers (using ctypes)
    input_data_ptr = input_value.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
    output_ptr = output_value.ctypes.data_as(ctypes.POINTER(ctypes.c_float))

    # Call the predict function
    status = predict(input_data_ptr, output_ptr)

    if status == 0:
        print("Prediction results:", output_value[0])
    else:
        print("Prediction failed.")

接下来, 创建predict.c 文件

// predict.c
#include <Python.h>
#include <stdio.h>

static PyObject *predict_load_model(PyObject *self, PyObject *args) {
    const char *model_path;
    if (!PyArg_ParseTuple(args, "s", &model_path)) {
        return NULL;
    }

    PyObject *pModule = PyImport_ImportModule("__main__");
    if (pModule == NULL) {
        return NULL;
    }
    PyObject *pFunc = PyObject_GetAttrString(pModule, "load_model");
    if (pFunc == NULL || !PyCallable_Check(pFunc)) {
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
        return NULL;
    }

    PyObject *pArgs = PyTuple_New(1);
    PyObject *pValue = PyUnicode_FromString(model_path);
    if (!pValue) {
        Py_DECREF(pArgs);
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
        return NULL;
    }
    PyTuple_SetItem(pArgs, 0, pValue);

    PyObject *pResult = PyObject_CallObject(pFunc, pArgs);
    Py_DECREF(pArgs);
    Py_XDECREF(pFunc);
    Py_DECREF(pModule);

    if (pResult == NULL) {
        return NULL;
    }

    Py_DECREF(pResult);  // We don't need the result itself.
    Py_RETURN_NONE;
}

// Function to perform the prediction
static PyObject *predict_predict(PyObject *self, PyObject *args) {
    float *input_data_ptr;
    float *output_ptr;

    if (!PyArg_ParseTuple(args, "OO", &input_data_ptr, &output_ptr)) {
        return NULL;
    }

    PyObject *pModule = PyImport_ImportModule("__main__");
    if (pModule == NULL) {
        return NULL;
    }
    PyObject *pFunc = PyObject_GetAttrString(pModule, "predict");
    if (pFunc == NULL || !PyCallable_Check(pFunc)) {
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
        return NULL;
    }

    PyObject *pArgs = PyTuple_New(2);
    PyTuple_SetItem(pArgs, 0, PyLong_FromVoidPtr(input_data_ptr));
    PyTuple_SetItem(pArgs, 1, PyLong_FromVoidPtr(output_ptr));

    PyObject *pResult = PyObject_CallObject(pFunc, pArgs);
    Py_DECREF(pArgs);
    Py_XDECREF(pFunc);
    Py_DECREF(pModule);

    if (pResult == NULL) {
        return NULL;
    }

    long status = PyLong_AsLong(pResult);
    Py_DECREF(pResult);

    return PyLong_FromLong(status);
}

static PyMethodDef PredictMethods[] = {
    {"load_model", predict_load_model, METH_VARARGS, "Load TensorFlow model."},
    {"predict", predict_predict, METH_VARARGS, "Perform prediction."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef predictmodule = {
    PyModuleDef_HEAD_INIT,
    "predict",   /* name of module */
    NULL, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    PredictMethods
};

PyMODINIT_FUNC
PyInit_predict(void)
{
    PyObject *m = PyModule_Create(&predictmodule);
    if (m == NULL)
        return NULL;

    return m;
}

同时创建setup.py文件

# setup.py
from setuptools import setup
from setuptools import Extension

predict_module = Extension(
    'predict',
    sources=['predict.c'], # We'll create predict.c next
    include_dirs=[],
    library_dirs=[],
    libraries=[],
    extra_compile_args=['-fPIC', '-std=c99'],  # Important for shared library
)

setup(
    name='predict',
    version='1.0',
    description='Python module for TensorFlow prediction',
    ext_modules=[predict_module],
)

编译.so文件

python3 setup.py build_ext --inplace

最后,是PHP的调用代码:

<?php

// 1. Define the FFI interface
$ffi = FFI::cdef(
    "
    int load_model(const char* model_path);
    int predict(float* input_data_ptr, float* output_ptr);
    ",
    __DIR__ . "/predict.so" // Path to the dynamic library
);

// 2. Load the model
$model_path = __DIR__ . "/linear_regression_model.h5"; // Model file path
$result = $ffi->load_model($model_path);
if ($result != 0) {
    echo "Failed to load model.n";
    exit(1);
}

echo "Model loaded successfully.n";

// 3. Prepare input data
$input_value = 10.0; // Example input value

// Create FFI arrays
$input_data_ptr = FFI::new("float[1]");
$output_ptr = FFI::new("float[1]");

// Copy data to FFI arrays
$input_data_ptr[0] = $input_value;

// 4. Perform prediction
$result = $ffi->predict($input_data_ptr, $output_ptr);

if ($result != 0) {
    echo "Prediction failed.n";
    exit(1);
}

echo "Prediction done.n";

// 5. Get the result
$output_value = $output_ptr[0];

echo "Prediction result: " . $output_value . "n";

?>

将以上代码保存到对应的文件中,运行PHP代码,即可看到输出结果。 这里的linear_regression_model.h5 会在第一次运行python代码的时候被创建。

12. FFI集成的优点和局限

FFI集成机器学习模型提供了性能优势,但需要仔细处理安全和复杂性。

13. 模型加载与预测:关键步骤的剖析

模型加载和预测是集成的核心,需要仔细处理数据类型和内存管理。

14. 性能优化的策略

优化模型和推理过程是提高性能的关键,可以采用模型优化、批量推理和缓存等策略。

发表回复

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