从 C 到 YOLO 推理:senyacnn 的双内核优化与数值验证
本文目录
推理优化不能只比较一组 FPS:当卷积累加顺序、批归一化折叠和输出布局都改变时,速度提升必须和数值差异一起验证。senyacnn 把 Darknet 风格的 cfg/weights 解析、C 语言卷积网络和 YOLO 检测后处理放在一个小型项目里,并保留原始与优化两条执行路径,专门用于正确性和性能 A/B 对比。
本文固定审阅默认分支提交 c8a6f32(2026-08-24)的 main.c、网络解析/前向、YOLO 解码、构建脚本和单元测试。仓库没有 README 或可见的许可证文件;当前 Git 树同时包含模型权重和 BMP 样例,因此不能仅凭源文件公开就推断这些二进制资产允许任意再分发。本文是静态代码复盘,没有声称在本机编译或取得新的模型精度/性能结果。
两条卷积执行路径
默认路径调用朴素 group-convolution 内核,保留历史浮点累加顺序,项目注释将它作为原始基准。-opt 必须放在第一个参数,开启另一条路径:
- 首次前向前把 batch normalization 参数折叠进卷积权重和 bias 副本;原始参数仍保留,避免优化过程覆盖基线数据。
- depthwise 卷积走按通道独立滑窗的专用实现,不把它误当成普通稠密卷积。
1×1卷积走跨通道点积专用实现,利用输入输出通道的连续布局。- 一般卷积先整理 im2col,再用四个独立累加器做
dot4,减轻单条浮点依赖链的延迟。
多个独立累加器会改变求和顺序,所以优化结果与原始结果出现小幅浮点差异并不自动意味着错误。合理门禁应按模型、层类型和样本规定容差,同时检查最终检测类别、置信度与框坐标;只比较输出图片哈希会把绘图、文件元数据等差异混进数值判断。
从配置到检测框
parser.c 读取 cfg 中的层类型、卷积参数、groups、BN 与 activation,统计 weights 文件所需浮点数,再解析 Darknet 权重并将每层参数接入网络结构。net.c 从 BMP 读取 BGR 像素,按目标尺寸做 letterbox,再拆成 RGB 的 CHW 连续平面并归一化;这是一条针对随附模型与 BMP 样例的窄输入路径,不是完整的数据服务或现代图像 I/O 封装。示例入口连续跑十次前向并在原图画出检测框。
layer_yolo.c 将每个网格/锚框的 objectness 和类别分数转换成候选框,nms.c 再做阈值过滤与重叠抑制。审阅时发现 parser 会从 mask 解析 anchor_num,但 YOLO 前向仍固定循环 3 个 anchor。当前示例模型若都使用三锚框,这个假设可能刚好成立;换成不同 mask 数量的 cfg 时,解析值和解码循环不一致,不能称作通用 Darknet cfg 兼容。扩展前应让解码循环使用解析后的数量,并为不同 anchor 数构造回归样例。
NMS 的当前实现还使用了 intersection / min(areaA, areaB) 这一重叠比例,而不是常见的 intersection / union IoU;阈值相同不代表两种定义产生相同保留框。比较模型输出或迁移到其它检测器前,应把度量名称和边界框坐标约定写清楚。
加载失败路径也需要防护:net_load 可能返回空指针,net_dump 能处理空值,但入口随后读取 mynet->bbox_num 前没有失败检查。配置或权重解析失败因此可能从可诊断错误变成空指针崩溃。cfg 解析还依赖固定长度缓冲区与 atoi,文件装载缺少面向不可信输入的完整长度校验;不要把任意上传文件交给当前解析器。
单元测试为什么可能假绿
项目开始为性能代码写独立数值基准,这是很有价值的工程习惯:test_dw.c 覆盖方形/非方形、stride、输出 pad 和较大通道数,并逐元素与朴素参考卷积比较;test_1x1.c 也针对含输出 padding 的布局做逐元素对照。测试注释还记录过参考实现与被测内核犯同一 padding 错误导致漏测的教训,提醒我们测试 oracle 本身也要独立审查。
但当前门禁信号不足以自动判定通过:
test_dw.c统计 mismatch 并打印DW TEST FAIL,最终仍固定return 0;调用方只看退出码会把失败当成功。test_1x1.c只打印最大差值,没有容差断言或非零失败状态。compare.sh将 depthwise 测试接到tail后面,管道状态可能只反映tail;优化/原始检测差异也以文本标签输出,不统一转换为最终失败退出码。
建议每个测试返回明确的 mismatch 数,失败时返回非零;shell 运行器启用严格错误传播,并把 build、每个单测、两模型 A/B 输出差异分别记录成独立结果。只看脚本最后显示了 TEST ALL PASS 或进程整体退出 0,并不足以确认每项比较真正通过。
构建与性能复现
build-linux.sh 与 build-win.sh 都调用 GCC;后者输出 .exe 不等于使用 MSVC 或完成原生 Windows 工具链验证。GitHub Actions 有跨平台构建/打包工作流,但没有构建并运行 test_dw、test_1x1 或 compare.sh,所以“CI 构建成功”不能说明数值回归通过。默认模型与样例文件名写在 main.c,优化开关必须处在首个参数位;比较脚本还会读写 /tmp 临时输出,重复运行前应确认不会覆盖需保留的文件。
入口计时使用单调时钟并将毫秒值转成 int,一次推理结果是十帧总耗时。跨平台比较时应记录 CPU、编译器和优化旗标、模型、输入图像、线程数、是否包含冷启动,并至少报告多轮分布;不能把十帧总耗时直接说成单帧端到端延迟。
下一步改进清单
- 让 anchor 解码使用 cfg 解析结果,加入 1/2/3/更多 anchor 的边界测试。
- 把所有数值测试改成机器可判定的断言与非零退出码,并让比较脚本在任一 build/test/diff 失败时停止。
- 保存原始与优化路径的张量级差异、检测级匹配率、阈值和容差,避免只有终端截图。
- 检查
net_load失败后的调用方行为、极端 cfg/权重尺寸、候选框容量上限和 BMP 输入错误路径。 - 为源码、模型权重、模型配置和样例图片分别说明来源及可再分发许可;将构建依赖、平台支持和已验证的硬件写进 README。
小结
senyacnn 的亮点不是模型规模,而是把朴素基线与 BN 折叠、depthwise/1×1 特化、dot4 优化放在同一 C 推理程序里,并尝试用小型参考实现捕捉内存布局错误。要让这种实验变成可靠交付,最先要补的不是更激进的 SIMD,而是可失败的测试门禁、cfg 与解码契约、资产许可说明和可复现的基准报告。
源仓库
本文核对的是默认分支固定提交 c8a6f32:cdyforever/senyacnn。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。