1. 项目缘起:为什么要在工业网关里塞进一个“大脑”
工业网关这个设备,干过现场的人都不陌生。它蹲在配电柜里、产线旁边、机柜角落,常年干的就是协议转换、数据采集、往上位机转发这些“体力活”。Modbus RTU 转 Modbus TCP、Profinet 转 MQTT、串口透传、4G 上传,这些是它的看家本领。但这两年现场的需求变了,客户不再满足于“把数据传上去”,而是开始问:能不能在网关本地就把异常判断出来?能不能不依赖云端,直接在设备侧做缺陷识别、振动分析、语音告警?
这个问题背后的核心矛盾很现实。工业现场的网络条件往往不稳定,把摄像头原始视频流或者高频振动数据全部推到云端,带宽扛不住,延迟也扛不住。一条产线每分钟出几十个工件,等云端推理结果回来,工件早就流走了。所以边缘AI推理这件事,从“锦上添花”变成了“刚需”。
我手上这台工业网关,配置说出来有点寒酸:四核 ARM 处理器,512MB 内存,8GB eMMC,跑着一个精简的 Linux 系统。这个配置放在服务器圈子里连“入门”都算不上,但工业网关的定位就是这样——低功耗、宽温、无风扇、7x24 小时运行。我要做的事情,就是在这个 512MB 内存的盒子里,跑通一条完整的边缘 AI 推理链路:从模型加载、输入预处理、推理执行到结果输出,全部本地完成。
为什么选这个方向?因为现场大量存量网关都是这个配置级别,如果我能在一台 512MB 内存的设备上把链路跑通,那这套方案就能复制到成千上万台已经在现场服役的设备上,不需要换硬件,只需要升级固件。这个价值比在实验室用一张高端显卡跑个 demo 要大得多。
这篇文章我会把整个链路的搭建过程拆开讲清楚:模型怎么选、运行时怎么挑、内存怎么省、推理怎么加速、踩了哪些坑。适合做工业物联网、边缘计算、嵌入式 AI 的同行参考,也适合刚接触边缘推理、想知道“小内存设备到底能不能跑 AI”的朋友。
2. 整体方案设计:512MB 内存下的取舍逻辑
2.1 先算一笔内存账,再谈方案选型
很多人做边缘 AI 的第一步就错了——先选模型,再看设备能不能跑。正确的顺序应该反过来:先算清楚设备有多少可用内存,再倒推能跑多大的模型。
这台网关标称 512MB 内存,但实际可用远没有这么多。系统启动后,内核、文件系统缓存、常驻的协议转换进程、网络栈已经吃掉了一大块。我实测了一下,系统空闲状态下free -m显示可用内存大约在280MB 到 320MB之间波动。这意味着留给 AI 推理链路的内存预算,撑死了200MB左右,还得留出安全余量,不能把内存吃满,否则 OOM Killer 随时会把你的推理进程干掉。
有了这个预算,方案选型就有了硬约束:
| 资源项 | 预算 | 说明 |
|---|---|---|
| 可用内存 | 约 200MB | 系统空闲后实测值,需留 30MB 余量 |
| 存储空间 | 约 4GB | eMMC 剩余空间,放模型和运行时 |
| CPU 算力 | 四核 ARM | 无独立 NPU 或 NPU 算力有限 |
| 推理延迟目标 | 单帧 200ms 以内 | 满足产线节拍要求 |
这张表就是整个方案设计的“宪法”,后面所有决策都要围绕它来做。
2.2 运行时选型:ONNX Runtime 与 llama.cpp 的分工
边缘推理的运行时选择,直接决定了内存占用和推理效率。我最终采用的是ONNX Runtime负责视觉类模型,llama.cpp负责轻量语言模型,两者分工明确。
为什么视觉模型用 ONNX Runtime?因为 ONNX 是一个开放的模型交换格式,PyTorch、TensorFlow 训练的模型都能导出成 ONNX,工具链成熟。ONNX Runtime 本身支持 ARM 架构,可以编译成精简版本,去掉不需要的算子,内存占用能压到几十兆。而且它支持 INT8 量化模型的直接加载,这对内存和算力都紧张的设备来说非常关键。
为什么语言模型用 llama.cpp?因为 llama.cpp 的设计哲学就是“在资源受限的设备上跑推理”。它用纯 C/C++ 编写,没有沉重的依赖,支持 GGUF 格式的量化模型,4-bit 量化后一个几亿参数的小模型可以压到几十兆。对于工业场景里的语音指令识别、简单问答、告警文本生成这类任务,完全够用。
这里要特别说明一点:llama.cpp 的 Python 安装在开发阶段很方便,pip install llama-cpp-python就能用,但部署到网关上的时候,我建议直接用编译好的 C++ 可执行文件,不要带 Python 运行时。Python 解释器本身就要吃掉二三十兆内存,在 512MB 的设备上这是奢侈的。开发阶段用 Python 调通逻辑,部署阶段换成 C++ 二进制,这是我踩过坑之后总结出来的做法。
2.3 模型选型:YOLOv8n 与量化小语言模型
视觉侧我选的是YOLOv8n,n 代表 nano,是 YOLOv8 系列里最小的版本。为什么不用更大的模型?因为网关场景下的视觉任务通常很聚焦——识别工件有无、判断指示灯状态、检测简单缺陷,不需要通用大模型那种泛化能力。YOLOv8n 在 COCO 数据集上 mAP 大概 37% 左右,听起来不高,但针对特定场景做微调之后,实际效果完全够用。
模型导出成 ONNX 之后,原始 FP32 版本大约 12MB。这个大小其实已经可以接受了,但我还是做了 INT8 量化,量化后压到3MB 出头,推理速度也提升了将近一倍。量化的代价是精度会掉一两个点,但在工业检测场景里,通过增加训练样本和调整置信度阈值,这点损失完全可以补回来。
语言模型侧,我选了一个参数量在 1B 以下的量化模型,用 GGUF 的 Q4_K_M 量化格式,文件大小控制在80MB 以内。这个尺寸的模型做不了复杂的逻辑推理,但做关键词提取、意图分类、模板化文本生成是绰绰有余的。工业场景里本来也不需要模型“吟诗作对”,能把“设备三号振动异常,建议检查轴承”这句话说清楚就行了。
2.4 整体链路架构
整条链路的数据流是这样的:传感器或摄像头采集原始数据,经过预处理模块(缩放、归一化、格式转换),送入推理引擎,推理结果经过后处理(非极大值抑制、阈值过滤、文本解码),最后通过网关的协议栈输出到 PLC 或者上传到上位机。
这个架构看起来简单,但每一环都有内存和算力的坑。比如图像预处理,如果用 OpenCV 的默认配置,光加载库就要占不少内存,我后来换成了轻量级的图像处理库,自己写了 resize 和归一化的函数,内存占用直接降了一半。再比如后处理里的非极大值抑制,如果候选框数量多,临时内存分配会很频繁,我改成了预分配缓冲区的方式,避免了运行时的内存抖动。
提示:在内存紧张的设备上,任何“运行时动态分配”都要警惕。能在初始化阶段预分配的,绝不放到推理循环里做。
3. 核心细节解析:从模型转换到内存优化的关键操作
3.1 模型导出与 INT8 量化的具体步骤
把 YOLOv8n 导出成 ONNX 并做 INT8 量化,是整个链路里最容易出问题的环节。我一步步说。
第一步,从训练框架导出 ONNX。假设你用的是常见的深度学习框架,导出命令大致如下:
# 导出 ONNX 模型,指定输入尺寸和动态轴 torch.onnx.export( model, dummy_input, "yolov8n.onnx", input_names=["images"], output_names=["output0"], dynamic_axes={"images": {0: 1}, "output0": {0: 1}}, opset_version=12 )这里有几个关键点。opset_version建议用 12 或 13,太新的版本 ONNX Runtime 在 ARM 上可能不支持,太老的版本又缺少一些必要的算子。dynamic_axes把 batch 维度设成动态的,这样部署时可以灵活调整 batch size,虽然边缘设备上通常 batch 就是 1。
第二步,INT8 量化。ONNX Runtime 提供了量化工具,但需要准备一批校准数据。校准数据的意义是让量化工具统计每一层激活值的分布,从而确定量化的缩放因子。校准数据不需要标注,但必须和实际推理时的数据分布一致。我用现场采集的 200 张图片做校准,量化后的精度损失控制在可接受范围内。
from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_input="yolov8n.onnx", model_output="yolov8n_int8.onnx", weight_type=QuantType.QInt8 )动态量化不需要校准数据,实现简单,但对激活值的量化是运行时进行的,精度和速度的平衡不如静态量化。如果追求极致性能,应该用静态量化,准备校准数据集,用quantize_static接口。我在网关上两种都试过,静态量化推理速度快大约 15%,但准备校准数据麻烦一些。最终我选了静态量化,因为工业场景对延迟敏感。
3.2 内存优化的五个实操手段
512MB 内存跑推理,内存优化是贯穿始终的主题。我总结了五个最有效的手段,按效果排序。
第一个手段:模型量化。这是效果最直接的。FP32 转 INT8,模型体积直接降到四分之一,推理时的内存占用也同步下降。而且 ARM 处理器对 INT8 运算有专门优化,速度提升明显。
第二个手段:算子裁剪。ONNX Runtime 编译时可以指定只保留用到的算子,把不需要的算子从二进制里去掉。我编译的时候只保留了 YOLOv8 和语言模型用到的算子集,编译出来的库文件从 40 多兆降到了18MB。这个操作需要在编译时配置,具体是在 CMake 里设置--minimal_build和自定义算子列表。
第三个手段:预分配内存池。ONNX Runtime 默认会在推理时动态分配内存,这在内存紧张的设备上容易造成碎片。我通过配置enable_cpu_mem_arena和设置arena_extend_strategy为kSameAsRequested,让运行时一次性申请一块大内存自己管理,避免了频繁的系统调用和内存碎片。
第四个手段:输入分辨率控制。YOLOv8n 默认输入是 640x640,我根据实际场景降到了416x416。分辨率降低后,中间特征图的内存占用下降了将近 60%,推理速度也快了。代价是小目标的检测精度会下降,但工业场景里检测目标通常占画面比例较大,影响可控。
第五个手段:推理循环里零分配。这是最容易被忽视的一点。很多人在写推理循环时,每次迭代都创建新的输入张量、新的输出容器,这些临时对象会不断触发内存分配和回收。我的做法是在循环外预分配好所有缓冲区,循环内只做数据填充和读取,不创建任何新对象。这个改动让内存曲线从“锯齿状波动”变成了“一条平线”。
3.3 llama.cpp 在网关上的编译与部署要点
llama.cpp 在开发机上用 Python 安装确实方便,但部署到网关上,我强烈建议走 C++ 编译路线。具体步骤:
首先,在交叉编译环境或者直接在网关上编译。网关的 ARM 处理器性能有限,编译过程可能要十几分钟,但这是一次性的工作。编译时开启针对 ARM 的优化选项:
cmake -B build -DCMAKE_BUILD_TYPE=Release \ -DLLAMA_NATIVE=ON \ -DLLAMA_ARM_FMA=ON \ -DLLAMA_AVX=OFF \ -DLLAMA_AVX2=OFF cmake --build build --config Release -j4这里LLAMA_NATIVE=ON让编译器针对当前 CPU 架构生成最优指令,LLAMA_ARM_FMA=ON启用 ARM 的浮点乘加指令。AVX 系列是 x86 的指令集,在 ARM 上必须关掉,否则编译会报错。这个坑我踩过,当时看到编译错误一脸懵,后来才反应过来是架构不匹配。
编译完成后,你会得到一个main可执行文件,这就是推理入口。模型文件用 GGUF 格式,通过命令行参数指定:
./main -m model-q4_k_m.gguf -p "设备振动异常" -n 64 --ctx-size 512--ctx-size控制上下文长度,工业场景下的文本通常很短,512 足够了。设太大反而浪费内存,因为上下文缓存是按这个尺寸预分配的。
注意:llama.cpp 的模型文件要放在 eMMC 上,不要放在 tmpfs 或者内存文件系统里。80MB 的模型文件如果加载到内存里,对 512MB 的设备来说是灾难性的。
3.4 输入预处理与后处理的轻量化实现
预处理和后处理这两个环节,看起来不起眼,但在资源受限设备上往往是性能瓶颈。
图像预处理我放弃了 OpenCV,自己用 C 写了一个极简的 resize 和归一化函数。OpenCV 功能强大,但库太大,加载慢,内存占用高。我的需求很简单:把摄像头采集的 YUV 或 RGB 数据缩放到 416x416,然后做归一化。自己写的话,双线性插值加一个除法,代码不到一百行,编译出来几 KB,内存占用几乎可以忽略。
后处理里的非极大值抑制,我用了预分配数组的方式。YOLOv8n 在 416x416 输入下,候选框数量最多也就几百个,我预分配了一个 1024 长度的数组,循环里只做原地操作,不动态扩容。这样整个后处理过程没有任何内存分配,速度非常稳定。
语言模型的后处理主要是文本解码和格式化。llama.cpp 输出的 token 需要转换成可读文本,然后根据业务规则提取关键信息。这部分逻辑我用 C++ 写了一个简单的状态机,避免了正则表达式库的依赖。
4. 实操过程:从零到跑通全链路的完整记录
4.1 环境准备与依赖安装
网关到手之后,第一件事是确认系统环境。我用的这台设备跑的是精简版 Linux,内核版本 5.x,glibc 版本比较老。先更新包管理器,然后安装编译工具链:
apt update apt install -y build-essential cmake git wget这里有个坑:精简系统的软件源可能不全,cmake 版本可能太老,不支持某些新特性。如果遇到这种情况,需要手动下载新版本的 cmake 编译安装。我当时的 cmake 版本是 3.10,编译 ONNX Runtime 时报错说不支持某个选项,后来升级到 3.22 才解决。
接下来是编译 ONNX Runtime。这一步是整个流程里最耗时的,在网关上直接编译可能要一两个小时。我的做法是在性能更好的开发机上交叉编译,然后把编译好的库文件拷贝到网关上。交叉编译工具链的配置这里不展开,核心是设置好CMAKE_TOOLCHAIN_FILE,指定目标架构为 aarch64。
编译 ONNX Runtime 时的关键配置:
./build.sh --config Release --arm64 \ --minimal_build \ --disable_ml_ops \ --disable_rtti \ --skip_tests \ --parallel 4--minimal_build去掉所有非必要的算子,--disable_ml_ops去掉机器学习相关的扩展算子,--disable_rtti关闭运行时类型信息以减小体积。这几个选项下来,库文件能小一半以上。
4.2 模型部署与首次推理测试
库编译好之后,把 ONNX Runtime 的动态库和量化后的模型文件拷贝到网关的/opt/edgeai/目录下。目录结构我整理成这样:
/opt/edgeai/ ├── lib/ │ └── libonnxruntime.so ├── models/ │ ├── yolov8n_int8.onnx │ └── tinyllm-q4_k_m.gguf ├── bin/ │ ├── vision_infer │ └── llama_infer └── config/ └── inference.conf首次推理测试,我写了一个最简单的 C++ 程序,加载模型,构造一个全零的输入张量,跑一次推理,打印输出形状和耗时。这一步的目的是验证运行时能正常加载模型,不涉及精度。
// 简化示意,实际代码包含错误处理和资源管理 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test"); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(2); session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); Ort::Session session(env, "yolov8n_int8.onnx", session_options); // 构造输入 std::vector<int64_t> input_shape = {1, 3, 416, 416}; std::vector<float> input_data(1 * 3 * 416 * 416, 0.0f); // 执行推理并计时 auto start = std::chrono::high_resolution_clock::now(); // ... 推理调用 ... auto end = std::chrono::high_resolution_clock::now();第一次跑通的时候,单帧推理耗时大约180ms,内存占用峰值在150MB左右。这个结果基本达标,但还有优化空间。
4.3 内存占用实测与调优过程
跑通之后,我用valgrind的 massif 工具做了一次内存剖析,看看内存到底花在哪里了。结果有点意外:模型本身只占了 3MB,ONNX Runtime 库占了 18MB,但推理时的峰值内存到了 150MB。多出来的内存主要是中间特征图和运行时的工作缓冲区。
针对这个情况,我做了三件事。第一,把输入分辨率从 640 降到 416,特征图内存直接降了六成。第二,配置 ONNX Runtime 的内存池策略,让它在初始化时一次性申请 80MB 的 arena,后续所有分配都从这个池子里拿。第三,把推理线程数从 4 降到 2,减少线程栈的内存开销。
调优之后,峰值内存降到了95MB左右,系统整体可用内存保持在 150MB 以上,安全余量充足。推理耗时反而略有下降,因为内存分配的开销减少了。
| 调优阶段 | 峰值内存 | 单帧耗时 |
|---|---|---|
| 初始版本 | 150MB | 180ms |
| 降分辨率后 | 110MB | 140ms |
| 内存池优化后 | 95MB | 125ms |
| 线程数调整后 | 92MB | 130ms |
线程数从 4 降到 2 之后,耗时略微回升,但内存更稳了。在工业场景里,稳定性比那几毫秒的延迟更重要。
4.4 语言模型推理的接入与联调
视觉链路跑通之后,我把 llama.cpp 也接了进来。语言模型的推理和视觉模型不太一样,它是自回归的,每生成一个 token 都要跑一次前向计算。在网关这种算力下,生成 64 个 token 大概需要2 到 3 秒。这个速度做实时对话肯定不行,但做异步的告警文本生成、日志摘要,完全够用。
联调的时候遇到一个问题:视觉推理和语言推理同时跑的时候,内存会互相挤占。虽然两个模型加起来不到 100MB,但两个运行时同时工作时的峰值内存会叠加。我的解决方案是分时复用:视觉推理是周期性的,每 500ms 跑一次;语言推理是事件驱动的,只在检测到异常时才触发。两者在时间上错开,避免了内存峰值叠加。
具体实现上,我用了一个简单的互斥锁加状态标志。视觉推理进行时,语言推理请求排队等待;语言推理进行时,视觉推理跳过当前周期。这个策略牺牲了一点实时性,但换来了内存的稳定。
5. 常见问题与排查技巧实录
5.1 模型加载失败与算子不支持的排查
在 ARM 平台上跑 ONNX 模型,最常见的问题就是算子不支持。ONNX Runtime 的 ARM 版本默认只包含一部分算子,如果你的模型用了比较新的或者冷门的算子,加载时就会报错。
排查方法很简单:先用 ONNX Runtime 的 Python 版本在开发机上加载模型,看能不能跑通。如果 Python 版本能跑,说明模型本身没问题,是 ARM 版本的算子缺失。这时候有两个选择:要么重新编译 ONNX Runtime,把需要的算子加进去;要么修改模型,用支持的算子替换掉不支持的。
我遇到过一次Resize算子不支持的情况,原因是用了opset_version=13的新特性。后来把 opset 降到 12,问题就解决了。所以导出模型的时候,opset 版本不要盲目追新,要和目标运行时的支持范围匹配。
5.2 内存不足导致的进程被杀与应对
OOM Killer 是边缘设备上最无情的杀手。你的推理进程跑得好好的,突然就没了,日志里只留下一句Killed。这种情况九成是内存超了。
排查步骤:先用dmesg | grep -i oom看内核日志,确认是不是 OOM。然后检查推理进程的峰值内存,用/usr/bin/time -v跑一次推理,看Maximum resident set size是多少。如果确实超了,就按前面说的五个手段逐一优化。
还有一个容易被忽视的点:内存泄漏。C++ 程序里如果 ONNX Runtime 的Ort::Value对象没有正确释放,内存会慢慢涨上去。我的做法是用 RAII 封装所有资源,确保离开作用域就释放。另外,定期用valgrind做一次泄漏检查,防患于未然。
5.3 推理结果不稳定的原因分析
推理结果时好时坏,这个问题比崩溃更让人头疼。我遇到过几种情况,原因各不相同。
第一种,输入数据格式不对。摄像头输出的可能是 BGR 格式,但模型训练时用的是 RGB,颜色通道反了,检测结果自然不对。这种问题通过可视化输入数据就能发现。
第二种,归一化参数不一致。训练时用的均值方差和推理时用的不一样,导致输入分布偏移。这个要仔细核对训练脚本和推理代码里的预处理参数。
第三种,量化导致的精度损失。INT8 量化后,某些层的输出偏差被放大,导致检测框位置偏移或者置信度异常。解决办法是调整量化策略,对敏感层保持 FP32,或者增加校准数据的多样性。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决手段 |
|---|---|---|---|
| 模型加载报错 | 算子不支持 | Python 端验证 | 降 opset 或重编译 |
| 进程被 Killed | 内存超限 | dmesg 查 OOM | 量化、降分辨率、内存池 |
| 推理结果异常 | 预处理不一致 | 可视化输入 | 核对归一化参数 |
| 推理速度慢 | 线程配置不当 | 调整线程数测试 | 设为 CPU 核心数一半 |
| 内存持续增长 | 资源未释放 | valgrind 检查 | RAII 封装 |
| 语言模型输出乱码 | 编码问题 | 检查 tokenizer | 统一 UTF-8 编码 |
提示:这张表建议打印出来贴在工位上,现场调试的时候能省不少时间。
5.5 几个只有踩过才知道的避坑经验
第一个经验:不要在网关上直接编译大项目。网关的 CPU 性能有限,编译 ONNX Runtime 这种大项目可能要几个小时,而且编译过程中内存占用很高,容易触发 OOM。正确做法是在开发机上交叉编译,只把产物拷过去。
第二个经验:eMMC 的读写速度是瓶颈。模型文件加载、日志写入这些操作,如果频繁读写 eMMC,不仅慢,还会影响 eMMC 寿命。我的做法是把日志写到 tmpfs 里,定期转存到 eMMC,模型文件只在启动时加载一次,常驻内存。
第三个经验:温度对推理速度有影响。工业网关通常没有主动散热,夏天机柜内温度可能到 60 度以上。ARM 处理器在高温下会降频,推理速度可能下降 20% 到 30%。如果对延迟有严格要求,要么加散热片,要么在软件层面做降频补偿。
第四个经验:电源质量影响稳定性。工业现场的电源往往不干净,电压波动、浪涌都会影响网关的稳定运行。我遇到过推理进程莫名其妙重启的情况,后来发现是电源纹波太大导致 CPU 异常。加了一个高质量的电源滤波器之后,问题就消失了。
6. 性能数据与扩展方向
6.1 最终性能指标汇总
经过一系列优化,最终在 512MB 内存的工业网关上,整条边缘 AI 推理链路的表现如下:
| 指标 | 数值 | 备注 |
|---|---|---|
| 视觉模型推理延迟 | 125ms | 416x416 输入,INT8 量化 |
| 语言模型生成速度 | 约 25 token/s | Q4_K_M 量化,512 上下文 |
| 视觉推理内存峰值 | 92MB | 含运行时和缓冲区 |
| 语言推理内存峰值 | 85MB | 含模型和上下文缓存 |
| 系统整体可用内存 | 150MB+ | 推理间隙实测 |
| 连续运行稳定性 | 72 小时无异常 | 室温 25 度环境 |
这个数据说明,512MB 内存跑边缘 AI 推理是完全可行的,关键是要把每一兆内存都花在刀刃上。
6.2 从单模型到多模型调度的扩展
当前方案是视觉和语言两个模型分时复用。如果后续要接入更多模型,比如振动分析、声音识别,就需要一个模型调度器。我的思路是用一个优先级队列管理推理请求,高优先级的任务(比如安全相关的检测)优先执行,低优先级的任务(比如日志摘要)在空闲时执行。
调度器的实现可以用简单的生产者-消费者模式,一个线程负责接收请求,一个线程负责执行推理,中间用环形缓冲区传递任务。环形缓冲区的大小要预先设定,避免动态内存分配。
6.3 模型热更新的实现思路
工业设备部署之后,模型可能需要更新。如果每次更新都重启网关,会影响产线运行。所以模型热更新是一个很实际的需求。
我的实现思路是双缓冲:内存里保留两份模型指针,一份是当前使用的,一份是待更新的。更新时,先加载新模型到备用指针,加载成功后原子性地切换指针,然后释放旧模型。整个过程推理不中断,只是切换的瞬间可能有一两帧的延迟波动。
这个方案的关键是内存要能同时容纳两份模型。对于 3MB 的视觉模型来说没问题,但对于 80MB 的语言模型,同时加载两份就有点紧张了。所以语言模型的热更新我改成了“先卸载再加载”的方式,中间有短暂的不可用窗口,但通过请求排队来平滑过渡。
6.4 一些个人体会
这套方案从开始折腾到稳定运行,前后花了大概三周时间。大部分时间不是花在写代码上,而是花在排查各种环境问题和内存问题上。边缘 AI 部署这件事,技术原理其实不复杂,难的是对资源约束的极致把控和对现场环境的充分理解。
我最大的体会是:不要用服务器的思维做边缘设备。在服务器上,内存不够就加,算力不够就堆 GPU。但在边缘设备上,每一兆内存、每一毫秒延迟都要精打细算。这种约束反而逼着你去理解推理链路的每一个细节,知道内存花在哪里,知道时间耗在什么地方。
另外,现场环境永远比实验室复杂。温度、电源、振动、电磁干扰,这些因素在实验室里都不存在,但在现场都是实实在在的挑战。所以方案设计的时候,一定要留足余量,不要把资源用到极限。留 30% 的余量,换来的是稳定运行,这笔账怎么算都划算。
最后分享一个小技巧:在网关上跑一个简单的健康检查脚本,每隔几分钟记录一次内存、CPU 温度、推理延迟,写到环形日志里。出问题的时候,这些数据就是最好的排查依据。我就是靠这个脚本发现了一个内存缓慢泄漏的问题,否则可能要等到设备跑了一周之后崩溃了才知道。