把人员入侵、烟火检测、垃圾分类这三个场景同时塞进一块RK3588上跑,听起来像是个堆算力的活儿,实际上真正卡人的不是“能不能跑”,而是“怎么安排活”。我最早接到这个需求时,第一反应也是上分离方案:一块板子做安防,一块板子做环保,后来发现现场根本没有那么多空间和供电口,才被迫在单块RK3588上做多任务融合。这一步走完,回头看其实挺值的。
这篇文章把我个人在单块RK3588 NPU上同时部署三路视觉AI任务的完整过程写下来,包括芯片资源的真实边界、模型怎么选怎么压、NPU时间片怎么分、内存带宽怎么省、以及中间踩过的几个典型坑。不适合只想跑Demo的人,适合真正要做量产级方案、被资源逼到墙角的朋友。
1. 三个任务同时跑,核心矛盾不在算力而在“调度”
先说结论:RK3588的NPU理论算力是6 TOPS,INT8精度下大概能同时跑起来一个YOLOv5s(人员入侵)、一个轻量烟火分类器、一个三分类垃圾检测网络。但如果你按顺序推理、一次只跑一个模型,那整个系统的实时性会非常难看——因为单帧视频进来,你要先做人形检测,再做烟火识别,再做垃圾分类,全部串行完成,一帧要吃掉三份延迟。视频流一来就是25帧/秒,串行方案直接崩。
所以“同时跑”的真正含义,不是让三个模型同时占着NPU执行,而是让NPU在时间维度上分片,CPU负责调度和预处理,让每一路任务有自己的流水线节奏。你可以把NPU想象成一条单车道,三辆车都要过,关键不是车道变宽,而是你给每辆车安排好了发车间隔和目的地出口。
从实测来看,RK3588 NPU对多模型的支持是有限度的,官方RKNN Toolkit里可以同时加载多个模型,但底层执行时依然是排队串行。真正决定“同时感”的是:多路视频采集、图像缩放与归一化这些重CPU操作被挪到了独立的处理线程里,NPU只做矩阵运算这一件事,进而把NPU的利用率压到接近100%。我最终实现的效果是:三路RTSP视频输入,每路分别跑自己的任务,整体CPU占用控制在65%~75%,NPU占用稳定在90%左右,帧率各自保持在12~18 FPS之间,这个水平对于安防和环保场景是够用的。
这里要注意的是,很多人一上来就陷入“NPU算力够不够”的焦虑,其实单块RK3588的瓶颈往往出在内存带宽和CPU预处理上。6 TOPS听起来不大,但配合2.4GHz的八核CPU和双通道LPDDR4X,合理分配流水线之后,三路任务并行完全可行。下一节先讲清楚NPU的硬件边界,再讲调度方案才有意义。
2. NPU硬件边界:6 TOPS算力下各模块分工与内存带宽陷阱
2.1 解码通道和图像缩放是隐藏CPU大户
RK3588内置了VPU视频编解码单元,可以硬解H.264/H.265,这一点是个巨大优势。三路1080p视频流,如果全部软解,四颗A76大核立刻被打满,留给AI推理调度的余量就没了。所以我第一件事是强制所有视频流走硬件解码,而且解码输出的格式不选常见的YUV420,而是直接要求VPU输出NV12格式,配合RKNN的zero-copy接口,image buffer可以直接送进NPU,减少一次CPU拷贝。
实测这对内存带宽的节省非常明显。LPDDR4X双通道理论带宽大概17GB/s,听起来很高,但多路视频、NV12转RGB、模型输入Tensor复制都在争抢这一带宽。我之前用GStreamer插件做颜色空间转换的时候,三路视频同时拉流直接导致NPU推理掉帧,后来发现不是NPU不行,而是DDR带宽被转换操作吃掉了。解决思路有两个方向:其一,把图像缩放交给VPU的scaler(RGA硬件缩放),不要用CPU的resize函数;其二,模型的输入尺寸尽量贴近实际缩放后尺寸,减少无谓的数据搬运。
另外,RK3588的NPU在内部有三个独立的NPU核心(实际上封装成NPU单体,内部可配置),SDM(System DMA Module)可以把数据直接从VPU搬运到NPU。但注意,不是所有版本的RKNN驱动都会自动启用zero-copy,你需要检查内核dts配置和RKNN runtime版本。我最初跑的rknn-toolkit2 1.5.0版本,zero-copy模式偶尔会报“invalid memory”错误,升到1.6.0之后才稳定下来。
2.2 A76与A55大小核的分配逻辑
RK3588的CPU是4×A76+4×A55。很多人在部署时习惯把所有线程丢给A76,结果温度飙升到85度以上,触发降频,NPU反而更卡。我最终的分配方案是:A55核专门跑轻量的后台调度、日志、统计线程;A76核中,两个核跑视频采集与预处理,一个核跑推理调度与结果后处理,一个核留着给系统和个人防护余量。
可能有人问,为什么不把预处理放到A55上?实测结果非常明确:A55单核性能只有A76的三分之一左右,图像缩放和归一化在A55上跑,延迟会高出接近两倍。唯一适合A55的是那些不追求实时性的周期性任务,比如每分钟输出一次统计报表、检查IPC连接状态。
还要注意RK3588的NPU与CPU共享同一个散热模块。如果你给NPU的压力太大,连续跑满二十分钟,CPU也会跟着遭殃。我后来在散热方面加了一个主动PWM风扇控制,用板载的温度传感器做反馈,设置温控阈值:55度以下风扇低速,65度以上全速。这个问题我在后面第6节会专门展开,因为很多开发板出厂的风扇策略是粗略的开关控制,根本压不住持续推理的发热。
2.3 NPU的INT8量化精度与激活函数限制
三个任务的模型都涉及一个不可回避的问题:RK3588的NPU对INT8量化模型支持最好,FP16虽然也支持,但吞吐反而降低。这不难理解,INT8走的是NPU内部的硬件加速单元,FP16在某些算子上会退回CPU模拟,速度差距非常明显。所以最终线上跑的所有模型都必须做PTQ量化(训练后量化,Post-Training Quantization)或QAT量化(量化感知训练)。
PTQ量化最容易踩的坑是“量化后精度崩溃”,特别是烟火检测这种小目标密集场景。YOLOv8的火苗目标通常只有几十个像素宽,量化后激活值的分布如果拉伸不合理,检出的置信度会掉到0.3以下。我采用的办法是:先把每类图像按亮度、对比度做直方图统计,然后用每个类别的代表性子集做量化校准(calibration),校准集不贪多,每类120~200张足够,关键是覆盖白天、夜晚、逆光、反光等极端场景。做完这一步,烟火检测在量化后的mAP只掉了2.1%,完全在可接受范围内。
另外一点,ReLU和Sigmoid这类激活函数在NPU上的实现是有限制的。ReLU可以融合到卷积层里,几乎没有额外开销;但Sigmoid在NPU上会变成查表操作,输出长度有限,跑分割类模型时会出现梯度噪声。垃圾分类模型我最后没有用Sigmoid头,而是把多标签概率全部做成Softmax,实测在NPU上的推理时间更短,精度也没有改变。
2.4 算力预算演算:三路任务各自的TOPS占用
为了验证“6 TOPS够不够用”,我做了一版算力预算表,给每个任务算清楚它的理论占用。以YOLOv8n(输入640×640)为例,在RK3588 NPU上单帧INT8推理时间大约是70ms左右,也就是单路占用约30%的NPU容量(按6 TOPS算);如果在A76核上跑MNN或NCNN,时间反而增加到150ms以上,所以NPU路线是必须的。烟火检测用轻量化的YOLOv5n或者自研的MobileNetV3-SSD,单帧输入416×416,INT8推理约35ms;垃圾分类用MobileNetV3-Small作为backbone加一个自定义分类头,输入224×224,单帧推理约15ms。三个任务单帧推理总耗时约120ms,看似已经超过了33ms(25FPS对应帧间隔),但由于三路视频流是异步的,实际每路任务的调度窗口可以交错开,最终单路体验保持在15~18FPS,不会出现同时三路都卡死的情况。
这里我要说明一下,算力预算只是理论参考,真正的影响因素很多,包括输入分辨率、批次大小(batch size)、量化方式、解码器占用,以及NPU频率策略。我在init阶段就把NPU定频到最高档,并关闭了自动调频的电源管理策略,避免NPU在低负载和满负载之间反复跳变,这个细节对延迟波动的影响非常大。
3. 模型选型与量化:YOLOv8n、轻量化烟火分类、垃圾分类三模型落地
3.1 人员入侵检测:为什么选YOLOv8n而不是YOLOv5s
人员入侵检测是三个任务里对精度和召回要求最高的,因为它直接关系“有没有人进入禁区”这个核心结果。我对比了YOLOv5s和YOLOv8n在RK3588上的表现:YOLOv5s精度略高,但模型体积大、推理速度慢,单帧要90ms以上;YOLOv8n在640×640输入下精度只损失了约2.7%,但推理速度可以压到70ms以内,而且自带解耦头,对重叠人体的处理比v5更好。
训练细节上,我用了大约8000张人员入侵数据,场景包括室内、室外、围栏边界、夜间红外、雨天等。重点优化了遮挡场景,因为入侵行为经常发生在围墙边缘,人体只有部分露出,很容易漏检。我在训练时做了随机遮挡增强(Random Erasing),同时给模型加了一个allow_person_in_zone的后处理逻辑:只对预设ROI区域内的检测框做告警,区域外的目标全部过滤,这样既降低误报率,也不影响NPU推理速度。
推理端,我设置的最小检测框宽高为30×30像素,小于这个尺寸的框直接丢弃。原因很实在,在1080p视频里,小于30像素的“人”还没有一只猫大,生成告警只会让安保人员疲劳,最终还是丢在系统里。对于ROI区域内的检测框,我还会做时间维度上的二次确认:连续三帧都命中,才触发真实告警,单帧误检不会直接上报。
3.2 烟火检测:解决小目标与“烟”的歧义问题
烟火检测是三个任务里最难调参的,难在“像烟的东西太多”:雾气、水蒸气、灰尘、夜景反光、汽车尾气都可能被误报成烟。火相对好办,颜色特征明显,但烟是半透明的、形状随时变化的,整片白灰色区域很难和阴天背景区分。
最终模型结构上,我用的是YOLOv5n+一个额外的颜色注意力分支:对输入帧先做全局平均颜色统计,如果帧中存在大量蓝白色系(偏灰白烟)且纹理梯度很低,就增加烟的类别的置信度。这个后处理不是模型学出来的,而是我手工定义的先验规则,效果非常显著,雾天误报率下降了一半以上。
量化方面,烟火模型用的是YOLOv5n的PTQ量化,校准集选了1200张,包含晴天白天、夜景打光、远距离小目标、近处大火堆等场景。特别需要强调的是,校准图像不能只用“干净的正常场景”,一定要混入“几乎像火的橙色落叶堆”和“灰色雾气背景”,这样量化后模型的边界才不会被挤压掉。上板之后,该模型的单帧推理耗时约35ms(输入416×416),ROC曲线的F1值约为0.82,还是让人满意的。
3.3 垃圾分类:三分类模型的蒸馏与压缩
垃圾分类在硬件上压力最小,但它对量和类的定义要求非常清晰。我做的三分类是:可回收物、厨余垃圾、其他垃圾,没有做细粒度材质分类,因为人员投放垃圾时往往是一个动作,镜头捕捉到的时间很短,细化到塑料、纸张、金属反而容易混淆。
模型选的是MobileNetV3-Small作为骨干,把最后的分类头改为3类输出。训练时用了约6000张现场拍摄的垃圾桶照片,包括不同光照、不同垃圾桶颜色、不同垃圾袋材质。为了让模型更小更快,我做了知识蒸馏:用一个ResNet50大模型做teacher,指导MobileNetV3-Small的学习,最终准确率达到91%。在RK3588 NPU上INT8量化后,模型大小只有4.7MB,单帧224×224推理仅需15ms,完全可以和其他任务交替执行。
这里有个部署上的小技巧:垃圾分类任务并不需要每一帧都推理,它只在“有人扔垃圾”的事件触发后才需要高帧率识别。所以我给这个模型设置了事件驱动式调度——当人员入侵检测检测到有人在垃圾桶附近连续停留超过0.81秒,才启动垃圾分类推理线程,其余时间这个模型完全不占NPU资源。看起来是“同时跑三个模型”,实际执行时很多周期是“一个主力+一个轻量”的组合,NPU压力远小于理论峰值。
3.4 RKNN-Toolkit2转换与板端适配清单
把上述三个模型从PyTorch转移到RK3588 NPU,我先把整套流程说清楚。你需要在X86机器上安装rknn-toolkit2,版本推荐最新稳定版(我用的是1.6.0)。转换步骤大致如下:
# 安装rknn-toolkit2的conda环境 conda create -n rknn python=3.8 pip install rknn-toolkit2-1.6.0-cp38-cp38-linux_x86_64.whl转换脚本的写法其实基本一致,核心类是RKNN()。针对三个模型分别需要不同的输入尺寸和量化配置:
from rknn.api import RKNN rknn = RKNN() # 配置均值与归一化,必须跟训练时保持一致 rknn.config(mean_values=[[128, 128, 128]], std_values=[[128, 128, 128]], target_platform='rk3588') # 加载ONNX模型 rknn.load_onnx(model='./yolov8n.onnx') # 量化校准,这里的dataset.txt里写的是校准图像路径列表 rknn.build(do_quantization=True, dataset='./dataset.txt') # 导出rknn格式模型 rknn.export_rknn('./yolov8n.rknn') rknn.release()转换完成后,板端推理的代码结构并不复杂。主要差异在于,板端要启用zero-copy和NPU亲和性配置,不能像X86上那样直接调Python接口。我最终用的是C++ API和RKNN的Python的混合方案:C++负责视频采集和推理调度,Python负责业务逻辑和结果上报。
板端运行前一定要检查一个关键点:dts里NPU的interrupt相关配置是否正确。RK3588的NPU和CPU之间通过mailbox通信,如果内核配置有误,推理会偶发卡死。我遇到过“跑十分钟一切正常,之后突然无法加载模型”的问题,最后查到是mailbox中断绑到了A55核但核心乱序导致死锁,把中断绑到A76后问题消失。这个排查过程比较长,后面第7节会展开。
4. 多路并行推理框架:我的两级流水线与NPU时间片控制
4.1 为什么不用官方多线程示例而改用事件驱动流水线
RKNN官方Demo里大多数是单模型多线程,每个线程独立加载模型,然后抢NPU时间。这种方式在任务少的时候能跑通,一旦三路模型同时启动,会出现严重的“饥饿现象”:某个线程连续占用NPU,其他线程饿死,帧率波动可以达到每秒7~20帧之间跳动,这对安防级应用是不可接受的。
我改成了两级流水线架构。第一级是输入采集,每个视频流对应一个采集线程,负责通过V4L2或RTSP拉流、用RGA做缩放和格式转换,然后放入一个大小固定的环形缓冲池(ring buffer)。第二级是推理调度器,它是一个独立的调度循环,按时间片轮询三个模型的执行队列,每个时间片结束后主动让出NPU给下一个模型。这样从宏观上看,三个模型确实在“交替执行”,但每一路的帧推进是匀速的,不会出现单路模型被饿死的情况。
这里最关键的是轮询时间片的长度设置。设得太短,模型频繁切换上下文,NPU的cache失效严重;设得太长,某一路任务会表现为明显的周期性掉帧。我最后把时间片设为5ms,每个模型每次最多推理一帧(但YOLOv8n一帧需要70ms),也就是说,一个模型在获得NPU后最多连续占用5ms,然后不管推理是否完成都要让出。这不是传统意义上的preemptive调度,而是“按帧切分+队列缓冲”的合作式调度。实测下来,三路任务的帧间隔抖动从最初的12ms降低到3ms以内。
4.2 内存池设计与NV12转RGB的zero-copy实现
多路推理最常见的内存问题是反复malloc/free导致的内存碎片。每个模型输入Tensor大小不同,连续跑上几个小时,系统内存碎片化会严重影响NPU的分配效率。我做了统一的预分配内存池,把所有推理所需的输入输出内存都在初始化阶段一次性分配好,运行时只复用这些内存块,不再向系统申请新内存。需要注意,RKNN的输入绑定有两种方式:普通模式和zero-copy模式。zero-copy模式下,输入Tensor指向的必须是物理连续内存,不能用常规malloc函数。这个功能rknn_toolkit2的Python封装不完整,C++接口里专门有心智的rknn_create_mem来创建物理连续内存,我用它来做统一管理。
NV12转RGB这一步放在CPU上跑是最浪费资源的行为,而且RGA的NV12转RGB功能对YOLO系列模型的输入格式并不是完全兼容的。我最后采用的是直接把NV12 buffer按Y通道传给模型——对,你没看错,灰度图跑YOLOv8n也能工作。原因是人员入侵和烟火检测本身不依赖颜色特征,YOLOv8n的第一个卷积层会把1通道补成3通道,精度影响大约在1.2%左右。只有在垃圾分类任务上才真正做RGB转换,但那一路输入只有224×224,带宽开销小得多。这种“分任务决定输入通道”的思路,比起每路都做RGB转换,整体CPU占用降低了约18%。
4.3 任务优先级:入侵最高,烟火次之,垃圾最低
三路任务不是平等的。从业务角度,人员入侵必须优先,它决定了整个系统的安防属性;烟火检测也很关键,但它对实时性要求不如入侵高,允许存在1~2秒的延迟;垃圾分类则完全不要求实时,只要事件发生后的3秒内能给出结果即可。
所以调度器里我设置了三个优先级队列,每个线程携带一个优先权值。NPU时间片轮转时,优先权值高的队列会被多分配一些时间片。具体参数上,入侵检测每帧的时间片权重为3,烟火检测为2,垃圾分类为1。入侵检测模型大、耗时也长,所以它需要更多时间,但即使排空了高优先级队列,低优先级任务也不会饿死——每个调度周期最低保证8ms的时间片给垃圾分类模型跑一次。
我用这种方式跑了一整天的连续压力测试:单路视频固定25FPS输入,系统平均每帧处理时间为41ms,三路误差不大于5%。入侵检测漏报为0,误报2次;烟火检测有一次雾气误报;垃圾分类识别准确率稳定在89%左右。这个结果已经超过了同场景下很多“一台设备一路关键任务”的传统方案。
5. 后端优化:DDR带宽分配、NPU频率锁存与散热策略
5.1 为什么内存频率锁到最高反而省电
RK3588的DDR控制器有自动调频功能,但自动调频的切换策略很保守。在多路视频+AI推理的高并发场景下,DDR频率在1066MHz和1566MHz之间快速跳变,反而会导致总线延迟增大。我在U-Boot或内核启动参数里把DDR频率锁定到1566MHz之后,整体推理稳定性明显提升。
有人会问高频率不是更费电吗?实际上,系统在高负载下如果DDR频率不足,CPU和NPU等待总线的时间变长,设备需要更长时间才能完成任务,耗电量反而更高。锁频后,任务在更短时间内完成,总功耗反而可能下降。实测下来,锁频到1566MHz后,三路任务同时运行时的系统总功耗在9.8W左右,比默认策略下的10.6W低了一些,温度最高也控制在71度以下。
5.2 风扇控制:从开关式改为PID调温
开发板常见的风扇只有一档开关,温度到达阈值就满转,降到阈值就停。这种控制方式在音频环境下(比如户外安防箱)会带来周期性噪音,而且风扇频繁启停容易损坏,最关键的是,风扇全速运转时如果散热片贴得不好,I2C读取到的温度可能不均匀,导致NPU局部过热。
我改成PID控温:温度感知通过板载的adc读热敏电阻或者直接读NPU温度寄存器,然后PID输出PWM占空比控制风扇转速。这套逻辑跑在A55核的一个低优先级线程里,每500ms读取一次温度。参数设置如下:目标温度60度,比例系数3.0,积分系数0.05,微分系数1.2。实际效果是温度在60~65度之间时,风扇转速平滑变化,不再出现忽快忽慢的现象。如果你不想自己写PID,使用hrtimer加简单P控制器也足够,关键是要让占空比变化幅度小一些,避免硬件响应太激进。
这里还要提一个坑:很多风扇模块的转速计信号线在接了PWM调速之后无法正确反馈转速,导致内核报告“fan stall”。如果你的开发板有硬件转速检测,最好在dts里给fan配置一个最低占空比(比如20%),让风扇始终处于转动状态,转速计信号就不会丢失。
5.3 VPU硬解码与RGA缩放的buffer零拷贝链路
零拷贝链路是整个系统提效的核心。我最初的实现是:VPU解码输出NV12 buffer,然后程序将它复制到NPU输入buffer,RGB转换后再复制一次,这些重复搬运在1080p下耗费大量带宽。改成零拷贝后流程变成了这样:VPU解码输出直接落到一块物理连续DDR内存,RGA从这块内存做缩放,缩放结果还是NV12格式,再直接映射给NPU输入。这样的链路只有在buffer都来自相同内存池时才能成立,所以整个系统的内存分配必须统一交给一个C++内存管理类来做,不能用OpenCV自带的mat.data去算。
RGA缩放还有个隐藏bug:RGA对输入输出图像宽高对齐有要求,1080p原图缩放到640×640时,如果direct mode下宽度不满足16字节对齐,输出会产生少量彩色条纹。我的解决方式是让RGA先缩放到一个宽度为640、高度为640的对齐尺寸,然后再用NPU处理,而不是直接缩放到模型要求的640×640。因为RGA支持输出尺寸不是16的倍数时会自动补齐,但补齐区域可能不是合法像素。实际上RGA内部的scale模式只要保证输入输出都是4字节对齐就行,640本身满足这个条件,所以后来我就直接缩放到640,没有再遇到过花屏。
5.4 CPU负载过高的隐藏来源:日志打印和RTSP断线重连
多路视频跑起来之后,如果日志打印做的太粗暴,CPU占用率能凭空多出8%~10%。每一帧推理结果都打印一串JSON日志,这种操作在开发调试时无所谓,但线上运行时绝对不能干。我把所有日志分为三级:DEBUG级只在手动开启时输出,INFO级只在上报告警时输出,ERROR级随时保留。同时把日志写入本地环形内存,定期批量写盘,而不是每行flush一次。这个小改动让系统的CPU占用率大约降了3个百分点。
另一个容易出问题的是RTSP断线重连。现场IPC偶尔会出现画面卡顿,如果程序里处理不好,重连逻辑会以极高频率反复尝试,导致网络栈和内存快速膨胀。我做了退避重连策略:第一次断线等500ms重连一次,失败后间隔翻倍,最多到10秒;同时最多保持两路断线重连队列,超过就丢弃旧任务。这套策略跑下来,整周的重连次数控制在个位数,没有再出现内存暴涨。
6. 推理时延抖动排查:从NPU中断绑核到cache一致性
6.1 现象:三次模型并发,单路推理偶发飙到200ms
系统刚上线跑了一周,同事反馈说入侵检测偶发卡顿,一帧画面卡住将近200ms。因为这是偶发问题,第一反应查网络,发现RTSP没有丢包,带宽也很充裕。随后查CPU负载,发现A76核负载在并发任务高峰期并不均衡,某个核长期满负载。排查到最后才意识到,问题出在NPU中断绑核和cache一致性上。
RK3588的NPU完成一次推理后会产生中断,驱动在中断处理里把结果copy到指定buffer,并唤醒用户态线程。如果中断自动分配到某个正在执行高优先级任务的A76核上,该核要同时处理用户态逻辑和中断,最坏情况下中断处理被推迟几十毫秒,整个推理管道就被拖住。解决方案是把NPU中断固定绑定到一个专用的A76核上,同时把该核的负载尽量压低——只允许它处理中断和轻量通信,不跑业务推理线程。
# 把NPU中断绑定到CPU4(假设CPU4是A76的某个核) echo 16 > /proc/irq/$(cat /proc/interrupts | grep rknpu | awk '{print $1}' | sed 's/://')/smp_affinity这里16是二进制10000,表示只允许CPU4处理。也可以用irqset工具设置亲和性。
6.2 cache一致性导致的结果错乱
另一个偶发问题比较隐性:模型推理结果在有时会“张冠李戴”。一次垃圾识别把可回收物识别成厨余垃圾,但不是每次都错,而且重新推理同样的图像结果又会变。排查发现是zero-copy模式下NPU输入buffer缓存一致性被破坏:CPU先写入了输入图像,但cache没有刷回主存;NPU读取时获取的是主存中的旧数据,导致推理输入是半新半旧的混合帧。这个错误的触发和CPU缓存行是否被逐出息息相关,所以问题随机而诡异。
解决办法是在每次初始化input buffer后,显式调用flush cache接口。rknn_zero_copy的运行模式要求用户端调用rknn_flush_mem,确保GPU/VPU写到内存的数据可见。这个接口在文档里有,但大多数示例代码里并不会写,因为它只有在多硬件共享同一物理内存时会触发。我踩完这个坑后,把flush操作加到了所有输入和输出buffer的访问边界的统一封装里,彻底规避了这个问题。
6.3 时间戳与帧同步:三路任务如何对齐到统一时钟
三路视频流来自不同的IPC,帧率即使都是25FPS,起始相位也不一致。如果各自用系统时间卡点,会出现同一时刻三路画面不是同一瞬间的问题,导致告警联动时很难判断人员、烟火、垃圾是否真的相关。我引入了统一的PTP同步时钟源,或者用主IPC的RTCP时间戳作为统一的时钟基准,每帧数据都记录一个单调递增的帧序号和时间戳。调度器在分发帧时,优先选择时间戳最接近当前时刻、同时保证每个视频流队列中有至少两帧余量的帧。这样做的代价是每路延迟平均增加了约15ms,但换来了非常稳定的多路事件联动能力。
如果现场没有PTP时钟源,也可以用NTP对时,但精度在局域网内一般只能到几十毫秒,够用但不完美。我自己最后的方案是直接使用开发板上的PPS模块外接GPS授时,精度到微秒级,这个对普通安防场景可能是锦上添花,但在需要对比多路画面判断是否同一物体时确实很重要。
7. 三模型长时间运行的专项踩坑与解决办法
7.1 rknn.model类的内存泄漏问题
在C++中用rknn_init反复创建和销毁模型对象时,如果不显式调用rknn_destroy,内存会逐渐膨胀。我一开始是在一个循环里加载模型做测试,结果跑了三个小时,进程的内存从120MB涨到了1.2GB。后来做内存池的管理时才意识到:每个模型初始化时都会分配独立的权重buffer和中间计算buffer,这些buffer不会被Python的垃圾回收机制回收。正确做法是每个模型只初始化一次,整个生命周期内复用同一个rknn_model实例;如果确实需要动态卸载模型,调用rknn_destroy后再重新初始化。在板上跑长期任务时,建议在启动脚本里先做一次“内存自检”,打印出进程峰值和常驻内存,确认没有泄漏再进入主循环。
7.2 烟火模型的过拟合与PWM风扇声音干扰
这个坑比较有意思,我在训练烟火模型时,把训练数据里的火焰图片全部采集自固定位置的一个红外摄像头,结果模型对“画面中央的火焰区域”学得非常好,但对“画面边缘出现火焰”识别能力极差。数据集的偏差导致模型在真实场景中频繁漏检。解决办法是从多个不同安装角度的摄像头重新采集数据,并做数据增强:随机水平翻转、随机裁剪、随机调整亮度和对比度。上板后漏检率降低了一半以上。
PWM风扇的问题则更加隐蔽。风扇的PWM信号频率在25kHz时听不见,但如果你把频率设置成1kHz左右,在某些麦克风(比如烟火检测相机自带的拾音器)上会产生明显的电磁干扰噪声,导致烟火检测的音频特征混乱。我在烟火检测里加了一个音频特征辅助分类器(火焰燃烧的声音有一定的频率特征),结果风扇的干扰直接把它带偏了。排查到源头后,把PWM频率调到25kHz,噪声消失。如果开发板的PWM不支持25kHz,可以考虑加一个RC滤波电路,但直接用更高频率是最省事的办法。
7.3 断电保护与文件系统损坏
RK3588开发板跑的Linux系统如果多次意外断电,根文件系统很容易损坏。因为AI推理的结果存储和日志写盘如果频繁发生,flash写入操作会增加,断电风险也随之升高。我在部署时把所有频繁读写的路径(日志、结果数据库)全部挂载到tmpfs上,避免对eMMC的频繁写入。需要持久化的数据,采用定时批量落盘,而不是每次推理结果都写一次。同时给系统加了看门狗,一旦程序卡死超过30秒自动重启,避免程序无响应导致的人工干预。这个在无人值守的安防场景里非常关键。
7.4 模型热更新与版本回滚机制
线上的模型不可能永远不迭代。烟火检测在换了新的摄像头之后,可能需要重新训练和更新模型。为了保证更新过程不中断业务,我把模型放在只读分区里,用符号链接指向当前模型版本号;发布新版本时,先上传模型文件到临时分区,做一个checksum校验,然后原子地切换符号链接,再重启推理线程。不要直接在运行中覆盖模型文件,否则推理线程可能在读取模型参数时拿到半截数据,导致程序崩溃。如果新模型效果不好,回滚的操作就是换回旧的符号链接,30秒内可以恢复。
这个机制看起来简单,但在现场调试时非常省心。我有一次在客户现场更新模型,上传完模型之后,只花了5分钟时间就完成了替换和重启,其他流程都不用动。离线更新包的设计也应该遵循同样的逻辑,尽量做成“一个文件夹一个版本号”的目录结构,不要跨版本混用依赖库。
8. 离线测试与现场验收:一张可复现的压测清单
跑完架构和调优之后,必须做一轮可复现的压测。我把整个压测过程整理成清单,方便你照着来,也可以作为项目验收报告的一部分。
- 视频源模拟:准备至少三路1080p H.264测试视频,包含人员入侵、烟雾、火焰、普通垃圾和干扰场景(非同一时间段),放到本地RTSP服务里,使用ffmpeg循环推流。
ffmpeg -re -stream_loop -1 -i test1.mp4 -c copy -f rtsp rtsp://192.168.1.100:8554/test1系统资源监控:在测试期间实时记录CPU、内存、NPU占用、DDR频率、核心温度、风扇占空比,以及每路任务的推理耗时、检测框坐标和置信度。这些数据不仅用于验收,也可以作为后续调优的基线。
24小时连续老化:保证三路视频源持续输入,不做人工干预,重点观察是否出现内存泄漏、线程死锁、RTSP断线、文件系统异常等问题。我一般会在老化前和老化后分别跑一遍全功能测试,确保系统在“累了”之后依然能正常工作。
极端场景:切换视频流到高码率低光照场景,测试模型在不同亮度下的表现。夜间的入侵检测是最大考验,需要额外做红外补光测试。烟火在暗光下容易和LED灯混淆,垃圾分类在夜间基本不可用,这些都要和用户提前约定好边界。
告警上报与联动测试:模拟真实告警链路,人员入侵触发后是否能在3秒内上报到平台,烟火告警是否联动录像和灯光,垃圾分类结果是否推送到管理端。因为这三路任务共用NPU,要特别验证“三个事件同时发生”时系统是否能全部正确上报。我在这个测试中发现过一个问题:当三个告警同时产生时,日志发送线程出现了顺序错乱,导致查询记录时间倒挂。解决办法是给每个告警加一个单调递增序号,上报平台后按序号恢复顺序,而不是依赖系统时间。
功耗与发热记录:用功耗仪记录整机平均功耗和峰值功耗,同时观察外壳温度。在密闭机箱内连续运行12小时后,如果温度超过75度,建议调整NPU频率策略或加强散热。工业场景建议加装导热硅脂和铜块,把NPU热量快速传导到机箱外侧。
这张清单做完,基本上可以确信这套单块RK3588方案是能扛住现场工况的。后面如果业务需要扩展新的模型,也只需要按照同样的方式处理数据、训练、量化、接入调度器即可。
我自己的体会是,把三个任务强行塞进一块板子,不是“资源不够硬凑”的妥协,而是一种倒逼出的架构进步。多路模型从各自为政变成共享调度、共享内存、共享散热,整个系统在运维上反而更简单了——一台设备一个IP,告警统一上报,版本一起升级。如果你的场景也对体积、功耗、成本有硬约束,这套方案可以给你一个非常实际的起点,剩下的就是针对你的现场数据再做一轮模型微调和量化校准。