linux_learn/learn/NPU/04-板端推理部署.md

15 KiB

04 — 板端推理部署详解

一、整体架构

┌─────────────────────────────────────────────────┐
│              保护装置应用层主线程                  │
│                                                  │
│  ┌─────────────────┐   ┌──────────────────────┐ │
│  │ 录波/采样模块    │   │ CAN 协议栈 (PRIO_BURST)│ │
│  │ (ADC 驱动)      │   │ can_protocol_push_*()  │ │
│  └───────┬─────────┘   └──────────▲───────────┘ │
│          │ 滑动窗口 buffer         │ 故障分类结果  │
│          ▼                        │              │
│  ┌──────────────────────────────┐ │              │
│  │    NPU 推理线程 (SCHED_FIFO) │─┘              │
│  │    • rknn_run()              │                │
│  │    • 1~3ms 延迟              │                │
│  │    • 双缓冲, 零拷贝           │                │
│  └──────────────────────────────┘                │
│                                                  │
│  ┌──────────────────────────────────────────────┐│
│  │  librknnrt.so (RKNN Runtime)                 ││
│  │  └─ /dev/rknpu 或 npu_transfer_proxy         ││
│  └──────────────────────────────────────────────┘│
└──────────────────────────────────────────────────┘

二、最简推理 Demo

code/inference_demo.c — 跑通后集成到保护程序:

/**
 * 电力波形故障识别 — RK3568 NPU 板端推理 Demo
 *
 * 编译:
 *   ${CROSS_COMPILE}gcc -o fault_infer inference_demo.c \
 *       -lrknnrt -lpthread -lm
 *
 * 运行:
 *   ./fault_infer fault_model.rknn test_waveform.npy
 *
 * 依赖:
 *   - librknnrt.so (NPU 运行时, 板端已预装)
 *   - rknn_api.h (从 SDK 的 include/ 路径获取)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <math.h>

#include "rknn_api.h"

/* ================================================================
 *  配置 (需与训练/转换时保持一致)
 * ================================================================ */
#define MODEL_INPUT_CH    1
#define MODEL_SEQ_LEN     128
#define MODEL_NUM_CLASSES 5

/* 归一化参数 (从训练集统计) */
#define NORM_MEAN  0.0f
#define NORM_STD   1.0f

/* 故障类型名称 */
static const char *fault_names[] = {
    "正常",
    "A相单相接地",
    "BC相间短路",
    "AB两相接地",
    "三相短路",
};

/* ================================================================
 *  1. 模型加载
 * ================================================================ */

typedef struct {
    rknn_context   ctx;
    rknn_input     inputs[1];
    rknn_output    outputs[1];
    rknn_tensor_attr input_attr;
    rknn_tensor_attr output_attr;
    uint8_t       *model_data;
    uint32_t       model_size;
} nnd_context_t;

int npu_init(npu_context_t *npu, const char *model_path)
{
    int ret;

    memset(npu, 0, sizeof(*npu));

    /* 1. 读取 .rknn 模型文件到内存 */
    FILE *fp = fopen(model_path, "rb");
    if (!fp) {
        perror("fopen model");
        return -1;
    }
    fseek(fp, 0, SEEK_END);
    npu->model_size = (uint32_t)ftell(fp);
    fseek(fp, 0, SEEK_SET);

    npu->model_data = (uint8_t *)malloc(npu->model_size);
    if (!npu->model_data) {
        fclose(fp);
        return -1;
    }
    size_t n = fread(npu->model_data, 1, npu->model_size, fp);
    fclose(fp);
    if (n != npu->model_size) {
        fprintf(stderr, "模型文件读取不完整\n");
        return -1;
    }
    printf("模型文件加载: %s (%u bytes)\n", model_path, npu->model_size);

    /* 2. 初始化 RKNN 上下文 */
    ret = rknn_init(&npu->ctx, npu->model_data, npu->model_size,
                    0, NULL);
    if (ret < 0) {
        fprintf(stderr, "rknn_init 失败: %d\n", ret);
        return -1;
    }
    printf("RKNN 初始化成功\n");

    /* 3. 查询输入属性 */
    npu->input_attr.index = 0;
    ret = rknn_query(npu->ctx, RKNN_QUERY_INPUT_ATTR,
                     &npu->input_attr, sizeof(npu->input_attr));
    if (ret < 0) {
        fprintf(stderr, "查询输入属性失败: %d\n", ret);
        return -1;
    }
    printf("输入: fmt=%d, n_dims=%d, dims=[%d,%d,%d,%d]\n",
           npu->input_attr.fmt,
           npu->input_attr.n_dims,
           npu->input_attr.dims[0],
           npu->input_attr.dims[1],
           npu->input_attr.dims[2],
           npu->input_attr.dims[3]);

    /* 4. 查询输出属性 */
    npu->output_attr.index = 0;
    ret = rknn_query(npu->ctx, RKNN_QUERY_OUTPUT_ATTR,
                     &npu->output_attr, sizeof(npu->output_attr));
    if (ret < 0) {
        fprintf(stderr, "查询输出属性失败: %d\n", ret);
        return -1;
    }
    printf("输出: fmt=%d, n_dims=%d, size=%d\n",
           npu->output_attr.fmt,
           npu->output_attr.n_dims,
           npu->output_attr.size);

    /* 5. 预设输入结构 (减少重复赋值) */
    npu->inputs[0].index        = 0;
    npu->inputs[0].type         = RKNN_TENSOR_FLOAT32;
    npu->inputs[0].size         = MODEL_INPUT_CH * MODEL_SEQ_LEN * sizeof(float);
    npu->inputs[0].fmt          = RKNN_TENSOR_NCHW;
    npu->inputs[0].pass_through = 0;  /* 不跳过预处理 */

    return 0;
}


/* ================================================================
 *  2. 预处理: 滑动窗口 + 归一化
 * ================================================================ */

/**
 * 从采样环形缓冲区提取一个窗口并归一化
 *
 * @param raw_samples    原始采样值 (环形缓冲区)
 * @param offset         窗口起始偏移
 * @param window_buf     输出: 归一化后的窗口 [1][1][128] (NCHW 格式)
 */
void preprocess_window(const float *raw_samples, int offset,
                       float *window_buf)
{
    /* Z-score 归一化 */
    for (int i = 0; i < MODEL_SEQ_LEN; i++) {
        float val = raw_samples[offset + i];
        window_buf[i] = (val - NORM_MEAN) / NORM_STD;
    }
    /* NCHW 格式已通过一维数组表达: [1][1][128] = window_buf[0..127] */
}


/* ================================================================
 *  3. 推理
 * ================================================================ */

/**
 * 执行一次 NPU 推理
 *
 * @param npu           NPU 上下文
 * @param window_buf    预处理后的波形窗口 [128] float
 * @param probs         输出: 各类别的概率 [NUM_CLASSES]
 * @param predicted     输出: 预测类别
 * @return 推理耗时 (ms), <0 表示错误
 */
float npu_inference(npu_context_t *npu,
                    const float *window_buf,
                    float *probs,
                    int *predicted)
{
    int ret;
    struct timespec t0, t1;

    /* 设置输入 */
    npu->inputs[0].buf = (void *)window_buf;
    ret = rknn_inputs_set(npu->ctx, 1, npu->inputs);
    if (ret < 0) {
        fprintf(stderr, "rknn_inputs_set 失败: %d\n", ret);
        return -1.0f;
    }

    /* 推理 */
    clock_gettime(CLOCK_MONOTONIC, &t0);
    ret = rknn_run(npu->ctx, NULL);
    clock_gettime(CLOCK_MONOTONIC, &t1);
    if (ret < 0) {
        fprintf(stderr, "rknn_run 失败: %d\n", ret);
        return -1.0f;
    }

    /* 获取输出 */
    npu->outputs[0].want_float = 1;  /* 要求输出 float 格式 */
    npu->outputs[0].buf = probs;
    ret = rknn_outputs_get(npu->ctx, 1, npu->outputs, NULL);
    if (ret < 0) {
        fprintf(stderr, "rknn_outputs_get 失败: %d\n", ret);
        return -1.0f;
    }

    /* 计算耗时 */
    float elapsed_ms = (t1.tv_sec - t0.tv_sec) * 1000.0f +
                       (t1.tv_nsec - t0.tv_nsec) / 1.0e6f;

    /* 释放输出 (必须调用,否则内存泄漏) */
    rknn_outputs_release(npu->ctx, 1, npu->outputs);

    /* Softmax + argmax */
    float max_prob = probs[0];
    int   max_idx  = 0;
    float sum = 0.0f;
    for (int i = 0; i < MODEL_NUM_CLASSES; i++) {
        /* 数值稳定的 softmax */
        float exp_val = expf(probs[i] - max_prob);
        probs[i] = exp_val;
        sum += exp_val;
    }
    for (int i = 0; i < MODEL_NUM_CLASSES; i++) {
        probs[i] /= sum;
        if (probs[i] > max_prob) {
            max_prob = probs[i];
            max_idx  = i;
        }
    }

    *predicted = max_idx;
    return elapsed_ms;
}


/* ================================================================
 *  4. 清理
 * ================================================================ */

void npu_deinit(npu_context_t *npu)
{
    if (npu->ctx) {
        rknn_destroy(npu->ctx);
        npu->ctx = 0;
    }
    free(npu->model_data);
    npu->model_data = NULL;
    printf("NPU 资源已释放\n");
}


/* ================================================================
 *  5. main — 演示连续推理流程
 * ================================================================ */

int main(int argc, char *argv[])
{
    if (argc < 2) {
        printf("用法: %s <model.rknn> [test_waveform.npy]\n", argv[0]);
        return 1;
    }

    const char *model_path = argv[1];

    /* ---- 初始化 NPU ---- */
    npu_context_t npu;
    if (npu_init(&npu, model_path) < 0) {
        return 1;
    }

    /* ---- 准备测试数据 ---- */
    float window_buf[MODEL_SEQ_LEN];
    float probs[MODEL_NUM_CLASSES];
    int   predicted;

    if (argc >= 3) {
        /* 从 .npy 文件加载 (跳过 128 字节 NPY 头) */
        printf("加载测试波形: %s\n", argv[2]);

        /* 简化: 直接读取非 NPY 格式的 float 数组 */
        FILE *fp = fopen(argv[2], "rb");
        if (fp) {
            fread(window_buf, sizeof(float), MODEL_SEQ_LEN, fp);
            fclose(fp);

            /* 归一化 */
            for (int i = 0; i < MODEL_SEQ_LEN; i++) {
                window_buf[i] = (window_buf[i] - NORM_MEAN) / NORM_STD;
            }
        } else {
            printf("无法打开文件, 使用随机数据演示\n");
            goto random_test;
        }
    } else {
random_test:
        /* 生成模拟波形数据 (方波 + 噪声, 用于演示) */
        srand(time(NULL));
        for (int i = 0; i < MODEL_SEQ_LEN; i++) {
            window_buf[i] = (i > 30 && i < 70) ? 2.0f : 0.0f;  /* 模拟突变 */
            window_buf[i] += (rand() / (float)RAND_MAX - 0.5f) * 0.1f;
        }
    }

    /* ---- 连续推理 ---- */
    printf("\n开始推理...\n");
    printf("%-10s %-6s %-12s %s\n", "次数", "类别", "置信度", "耗时");

    for (int run = 0; run < 20; run++) {
        float elapsed = npu_inference(&npu, window_buf, probs, &predicted);

        if (elapsed < 0) break;

        printf("%-10d %-6d %-12s %-6.2f ms\n",
               run + 1,
               predicted,
               fault_names[predicted],
               (double)elapsed);

        /* 模拟滑动窗口: 偏移半个窗口 (64点) */
        float shifted[MODEL_SEQ_LEN];
        memcpy(shifted, window_buf + 64, (MODEL_SEQ_LEN - 64) * sizeof(float));
        for (int i = 0; i < 64; i++)
            shifted[MODEL_SEQ_LEN - 64 + i] =
                (rand() / (float)RAND_MAX - 0.5f) * 0.1f;
        memcpy(window_buf, shifted, sizeof(window_buf));
    }

    /* ---- 清理 ---- */
    npu_deinit(&npu);

    return 0;
}

三、编译与运行

3.1 Makefile

# NPU 推理 Demo — Makefile
# 在 RK3568 板端编译: make CROSS_COMPILE=aarch64-buildroot-linux-gnu-

CROSS_COMPILE ?=
CC            = $(CROSS_COMPILE)gcc
CFLAGS        = -Wall -O2 -std=gnu99
LDFLAGS       = -lrknnrt -lpthread -lm

TARGET        = fault_infer
SRCS          = inference_demo.c

all: $(TARGET)

$(TARGET): $(SRCS)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

clean:
	rm -f $(TARGET)

test: $(TARGET)
	./$(TARGET) deploy/fault_model.rknn

3.2 运行

# 编译
make

# 运行 (需要 root 权限访问 NPU)
sudo ./fault_infer deploy/fault_model.rknn

# 输出示例:
# 模型文件加载: deploy/fault_model.rknn (29184 bytes)
# RKNN 初始化成功
# 输入: fmt=2, n_dims=4, dims=[1,1,128,1]
# 输出: fmt=2, n_dims=2, size=20
#
# 开始推理...
# 次数       类别   置信度        耗时
# 1          2      BC相间短路    1.82 ms
# 2          2      BC相间短路    1.65 ms
# 3          5      Normal        1.71 ms
# ...
# NPU 资源已释放

四、集成到保护程序的关键修改

// === 在你的保护程序主循环中 ===

// 1. 初始化 NPU (程序启动时)
npu_context_t npu;
npu_init(&npu, "/opt/protect/fault_model.rknn");

// 2. 注册信号处理 (优雅退出时释放 NPU)
signal(SIGTERM, sig_handler);
// void sig_handler(int sig) { npu_deinit(&npu); exit(0); }

// 3. 主循环 (与采样和 CAN 协议栈协作)
float ring_buffer[1024 * 1024];  // 环形缓冲区
int   ring_pos = 0;

while (running) {
    // 等待新采样数据...
    // ring_buffer[ring_pos] = new_sample;
    // ring_pos = (ring_pos + 1) % BUF_SIZE;

    if (ring_pos >= MODEL_SEQ_LEN && ring_pos % 64 == 0) {
        // 每 64 点跑一次推理 (滑动窗口 50% 重叠)
        float window[MODEL_SEQ_LEN];
        preprocess_window(ring_buffer, ring_pos - MODEL_SEQ_LEN, window);

        float probs[MODEL_NUM_CLASSES];
        int   predicted;
        float elapsed = npu_inference(&npu, window, probs, &predicted);

        if (predicted != 0) {  // 非 Normal
            // 通过 CAN 协议栈突发上送故障信息
            can_protocol_push_yx_burst(
                FAULT_ADDR,         // 故障信号地址
                (uint8_t)predicted, // 故障类型编码
                get_time_ms()       // 时标
            );

            // 如果是严重故障 (相间短路/三相短路)
            if (predicted >= 2) {
                // 可以触发录波、保护动作等
                trigger_wave_recording();
            }
        }
    }
}

// 4. 清理
npu_deinit(&npu);

五、性能优化

5.1 零拷贝模式(生产环境)

/* 使用 rknn_set_io_mem 替代 rknn_inputs_set */
// 需要先分配 DMA 物理连续内存
#include <dma-buf.h>

int dma_fd = dma_buf_alloc(MODEL_SEQ_LEN * sizeof(float), O_RDWR);
void *dma_buf = mmap(NULL, size, PROT_READ | PROT_WRITE,
                      MAP_SHARED, dma_fd, 0);

rknn_tensor_mem mem;
mem.virt_addr = dma_buf;
mem.fd        = dma_fd;
mem.size      = MODEL_SEQ_LEN * sizeof(float);

// 绑定到 NPU 输入
rknn_set_io_mem(npu.ctx, RKNN_TENSOR_INPUT, &npu.input_attr, &mem);

// 之后直接往 dma_buf 写数据, 调用 rknn_run 即可
// 不需要每次 rknn_inputs_set, 省去一次 memcpy

5.2 双缓冲流水线

Buffer A (预处理) ──→ NPU 推理 ──→ 结果处理
                      │
Buffer B (预处理) ──→ (等待) ──→ NPU 推理 ──→ ...

将预处理和 NPU 推理时间重叠,吞吐量翻倍。

5.3 预期性能

指标 预期值
单次推理延迟 1~3 ms
连续推理吞吐 300~500 fps
内存占用 (模型) 30~100 KB
内存占用 (运行时) ~2 MB
CPU 占用 (推理期间) < 5% (NPU 计算, CPU 等待)