ggml 实战:用 YOLOv3-tiny 与 GGUF 权重实现 CPU/GPU 目标检测
ggml 实战用 YOLOv3-tiny 与 GGUF 权重实现 CPU/GPU 目标检测【免费下载链接】ggmlTensor library for machine learning项目地址: https://gitcode.com/GitHub_Trending/gg/ggml本篇基于 ggml 官方示例 examples/yolo/README.md 展开讲解如何用 ggml 张量库加载预训练的 YOLOv3-tiny 权重、完成 Darknet.weights到 GGUF 格式的转换并跑通一次完整的单图目标检测流程。读完本文你既能照做复现出带检测框的predictions.jpg结果也能从源码层面理解该示例中计算图的构建方式、卷积-归一化-激活的算子编排以及 YOLO 后处理锚框解码、letterbox 校正、NMS在 examples/yolo/yolov3-tiny.cpp 中的具体实现。示例定位与目录结构YOLO 示例位于examples/yolo/目录是 ggml 仓库中用“现成权重 少量 C 代码”搭起完整 CNN 推理管线的典型样本目录内容如下文件作用examples/yolo/yolov3-tiny.cpp主程序加载模型、构建计算图、前向推理、后处理与可视化examples/yolo/convert-yolov3-tiny.py将 Darknet 原始.weights二进制转换为 GGUF 文件examples/yolo/yolo-image.cpp / examples/yolo/yolo-image.h图像读写stb_image、letterbox 预处理、画框与标签绘制examples/yolo/data/coco.namesCOCO 80 类类别名每行一个如dog、carexamples/yolo/data/labels/绘制检测标签文字用的位图字体8 种字号 × 96 个 ASCII 字符examples/yolo/CMakeLists.txt定义可执行目标yolov3-tiny从构建配置看yolov3-tiny目标由 examples/yolo/CMakeLists.txt 定义仅链接ggml与示例公共库common两个源文件yolov3-tiny.cpp与yolo-image.cpp即构成全部实现。与gpt-2、mnist等其他示例不同yolo 子目录在 examples/CMakeLists.txt 中是无条件加入构建的不依赖GGML_BACKEND_DL开关单独构建整个仓库standalone 模式时示例默认开启。构建方式# 配置standalone 模式下默认 GGML_BUILD_EXAMPLESON可执行文件输出到 build/bin cmake -B build # 只构建 yolo 目标 cmake --build build --target yolov3-tiny -j产物位于build/bin/yolov3-tiny。主程序入口会先调用ggml_backend_load_all()动态加载所有已编译后端CPU 默认开启CUDA/Metal/Vulkan 等需编译期通过对应GGML_*选项开启随后按“优先 GPU、回退 CPU”的策略选择设备详见 examples/yolo/yolov3-tiny.cpp#L558-L597。获取权重并转换为 GGUF 格式README 给出的标准流程分三步下载原始权重、校验、转换。1. 下载 YOLOv3-tiny 预训练权重YOLOv3 原作者公开的 Darknet 格式文件并可用 SHA1 校验完整性$ wget https://pjreddie.com/media/files/yolov3-tiny.weights $ sha1sum yolov3-tiny.weights 40f3c11883bef62fd850213bc14266632ed4414f yolov3-tiny.weights2. 转换为 GGUF$ ./convert-yolov3-tiny.py yolov3-tiny.weights yolov3-tiny.weights converted to yolov3-tiny.gguf脚本依赖仓库requirements.txt中的gguf与numpy包。转换完成后可跳过下载原始权重直接获取社区已转换好的yolov3-tiny.ggufHuggingFace 上 rgerganov/yolo-gguf 仓库提供同一文件。转换脚本做了什么examples/yolo/convert-yolov3-tiny.py 按 Darknet 二进制的线性内存布局依次读取每一层卷积的参数。对每个卷积层save_conv2d_layer()先后读取biases偏置长度 输出通道数scales、rolling_mean、rolling_varianceBatchNorm 的仿射缩放与运行统计量以(1, filters, 1, 1)的形状写入 GGUF卷积核weights形状为(filters, inp_c, size, size)。值得注意的是脚本中的一行注释convert-yolov3-tiny.py#L20-L21ggml doesnt support f32 convolution yet, use f16 instead—— 卷积核在写入前被强制转换为float16即该示例模型中的卷积权重以 F16 存储输入张量则为 F32计算时由 ggml 后端负责按需转换。这一设计也解释了为什么 README 的运行时输出中出现 CUDA 初始化信息F16 权重与 GPU 后端配合是该示例的推荐路径。脚本对 13 个卷积层l0l12硬编码了输入/输出通道数3→16→32→64→128→256→512→1024→256→512→255 / 256→128→256→255其中两个检测头l91×1 卷积512→255与l12256→255传入了batch_normalizeFalse即不写 BN 参数——这与 YOLOv3-tiny 网络图中“最后一个卷积直接输出回归量、不做归一化”的结构一致。文件开头跳过 20 字节Darknet 的major/minor/revision头最后通过gguf_writer.write_header_to_file() / write_kv_data_to_file() / write_tensors_to_file()三步写出标准 GGUF 文件与仓库核心实现 src/gguf.cpp 及格式说明 docs/gguf.md 相对应。运行目标检测准备一张测试图片README 使用 Darknet 仓库自带的dog.jpg内含狗、车、自行车等目标在能访问data/目录的位置通常即examples/yolo/执行$ ./yolov3-tiny -m yolov3-tiny.gguf -i dog.jpgREADME 中记录的完整运行输出CUDA 后端load_model: using CUDA backend ggml_cuda_init: GGML_CUDA_FORCE_MMQ: no ggml_cuda_init: GGML_CUDA_FORCE_CUBLAS: no ggml_cuda_init: found 1 CUDA devices: Device 0: NVIDIA T1200 Laptop GPU, compute capability 7.5, VMM: yes Layer 0 output shape: 416 x 416 x 16 x 1 Layer 1 output shape: 208 x 208 x 16 x 1 Layer 2 output shape: 208 x 208 x 32 x 1 Layer 3 output shape: 104 x 104 x 32 x 1 Layer 4 output shape: 104 x 104 x 64 x 1 Layer 5 output shape: 52 x 52 x 64 x 1 Layer 6 output shape: 52 x 52 x 128 x 1 Layer 7 output shape: 26 x 26 x 128 x 1 Layer 8 output shape: 26 x 26 x 256 x 1 Layer 9 output shape: 13 x 13 x 256 x 1 Layer 10 output shape: 13 x 13 x 512 x 1 Layer 11 output shape: 13 x 13 x 512 x 1 Layer 12 output shape: 13 x 13 x 1024 x 1 Layer 13 output shape: 13 x 13 x 256 x 1 Layer 14 output shape: 13 x 13 x 512 x 1 Layer 15 output shape: 13 x 13 x 255 x 1 Layer 18 output shape: 13 x 13 x 128 x 1 Layer 19 output shape: 26 x 26 x 128 x 1 Layer 20 output shape: 26 x 26 x 384 x 1 Layer 21 output shape: 26 x 26 x 256 x 1 Layer 22 output shape: 26 x 26 x 255 x 1 dog: 57% car: 52% truck: 56% car: 62% bicycle: 59% Detected objects saved in predictions.jpg (time: 0.057000 sec.)输出的每一行Layer N output shape来自 examples/yolo/yolov3-tiny.cpp#L389-L392 的print_shape()格式为w x h x channels x batch行号沿用 Darknet 网络图的层编号16/17是池化/跳接连线故直接跳号255 3 × (4 80 1)对应每个网格点 3 个锚框的(4 个框参数 80 个类别 1 个 objectness)回归量。检测结果逐行打印类别与置信度最终带框结果写入predictions.jpg。命令行参数源码中的定义见 examples/yolo/yolov3-tiny.cpp#L482-L489 与yolo_print_usage()参数长选项说明默认值-h--help打印帮助并退出--d--device指定设备名未知设备名会列出所有可用设备及其显存空GPU 优先回退 CPU-t--threadsCPU 后端线程数硬件并发数 / 2至少 1-th--thresh检测阈值取值范围(0, 1]越严召回越低0.5-m--modelGGUF 模型路径yolov3-tiny.gguf-i--inp输入图片路径input.jpg-o--out输出图片路径predictions.jpg注意两个隐含的运行时依赖程序会固定读取相对路径data/coco.names并要求恰好 80 行源码中以GGML_ASSERT(labels.size() 80)强制校验以及data/labels/size_char.png字体图8 个字号 × ASCII 32~126因此直接以源码目录布局运行即可无需额外拷贝。模型加载GGUF 权重如何进入后端显存load_model()examples/yolo/yolov3-tiny.cpp#L77-L139展示了 ggml 中“从文件到设备”的标准加载范式用gguf_init_from_file()在临时上下文中解析 GGUF 元数据与张量新建目标上下文逐张量ggml_dup_tensor()复制结构元数据在 CPU 端no_alloctrueggml_backend_alloc_ctx_tensors()一次性为全部权重张量在后端 buffer 中分配内存遍历张量用ggml_backend_tensor_set()把 GGUF 里的数据逐块拷贝到设备README 输出中的load_model: using CUDA backend即来自此阶段的create_backend()日志按层号l{i}_weights / l{i}_biases / ...把张量挂到conv2d_layer结构体上并按 YOLOv3-tiny 的网络约定配置特殊层l7/l9/l10/l12的padding 0l9/l12关闭 BatchNorm 与激活线性输出。conv2d_layer结构yolov3-tiny.cpp#L23-L32同时持有weights / biases / scales / rolling_mean / rolling_variance五组张量和三个行为开关padding、batch_normalize、activate这为下面统一的图构建函数打下了基础。前向计算图13 个卷积层如何拼成 YOLOv3-tiny核心在build_graph()examples/yolo/yolov3-tiny.cpp#L394-L454。它先在图上下文中声明一个416 x 416 x 3的 F32 输入张量input对应转换时写死的网络分辨率model.width model.height 416然后按 Darknet 的网络定义串起算子// 单个卷积层的展开方式apply_conv2d struct ggml_tensor * result ggml_conv_2d(ctx, layer.weights, input, 1, 1, pad, pad, 1, 1); if (layer.batch_normalize) { result ggml_sub(ctx, result, ggml_repeat(ctx, layer.rolling_mean, result)); result ggml_div(ctx, result, ggml_sqrt(ctx, ggml_repeat(ctx, layer.rolling_variance, result))); result ggml_mul(ctx, result, ggml_repeat(ctx, layer.scales, result)); } result ggml_add(ctx, result, ggml_repeat(ctx, layer.biases, result)); if (layer.activate) { result ggml_leaky_relu(ctx, result, 0.1f, true); }见 examples/yolo/yolov3-tiny.cpp#L171-L184从源码结构看这里有几个值得学习的 ggml 用法BatchNorm 推理展开不单独调用归一化算子而是把(x - mean) / sqrt(var) * scale bias展开为ggml_sub / ggml_div / ggml_sqrt / ggml_mul / ggml_add的组合并用ggml_repeat把(1, C, 1, 1)的统计量广播到整个特征图。对 LLM 类模型而言类似的“把权重变换折叠成线性算子”是常见技巧Leaky ReLU 斜率 0.1与 Darknet 中 YOLO 卷积的标准配置一致两个检测头l913×13 特征图与l1226×26 特征图输出均被ggml_set_output()标记并命名为layer_15/layer_22最后分别通过ggml_build_forward_expand()展开进同一张ggml_cgraph一次ggml_backend_graph_compute()即可同时拿到两路预测。整体拓扑与运行输出逐行对应input 416x416x3 └─ l0 conv(3→16) → pool → l1 conv(16→32) → pool → l2 conv(32→64) → pool → l3 conv(64→128) → pool → l4 conv(128→256) ← layer_8 (26x26x256) → pool → l5 conv(256→512) → pool(保持13) → l6 conv(512→1024) → l7 conv1x1(1024→256) ← layer_13 (13x13x256) → l8 conv(256→512) → l9 conv1x1(512→255) ⇒ 输出 layer_15 (13x13x255) layer_13 → l10 conv1x1(256→128) → upscale 2x(最近邻) → concat(layer_8) (26x26x384) → l11 conv(384→256) → l12 conv1x1(256→255) ⇒ 输出 layer_22 (26x26x255)图构建完成后main()使用ggml_gallocr来自 include/ggml-alloc.h做计算图内存分配ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend))按后端默认 buffer 类型创建分配器并ggml_gallocr_alloc_graph()这是 ggml 官方推荐的图内存管理方式。图像预处理letterbox 与张量布局detect()examples/yolo/yolov3-tiny.cpp#L456-L461每帧先把原图送入letterbox_image(img, 416, 416)再把结果写入图输入张量yolo_image sized letterbox_image(img, model.width, model.height); struct ggml_tensor * input ggml_graph_get_tensor(gf, input); ggml_backend_tensor_set(input, sized.data.data(), 0, ggml_nbytes(input));letterbox_image()examples/yolo/yolo-image.cpp#L144-L160的实现要点按比例缩放至 416×416 内接矩形保持长宽比其余区域以灰色 0.5 填充避免拉伸形变缩放采用双线性插值resize_image()的横向先行 纵向混合图像数据以CHW布局data[c*w*h y*w x]存储、像素值归一化到[0, 1]与ggml_new_tensor_4d(ctx, GGML_TYPE_F32, w, h, 3, 1)的(ne0x, ne1y, ne2channel, ne3batch)内存序严格对应——这是ggml_backend_tensor_set()能直接整块拷贝的前提。读取图片用 stb_imageexamples/yolo/yolo-image.cpp#L66-L89固定转 3 通道。后处理锚框解码、letterbox 校正与 NMS推理结束后两路输出进入yolo_layeryolov3-tiny.cpp#L43-L65构造时用ggml_backend_tensor_get()把设备端预测拉回主机内存。两个检测头的配置yolov3-tiny.cpp#L468-L476yolo_layer yolo16{ 80, {3, 4, 5}, {10,14, 23,27, 37,58, 81,82, 135,169, 344,319}, layer_15}; // 13x13 粗网格 yolo_layer yolo23{ 80, {0, 1, 2}, {10,14, 23,27, 37,58, 81,82, 135,169, 344,319}, layer_22}; // 26x26 细网格即 12 个锚框按 mask 拆分粗网格用第 3/4/5 号大目标细网格用第 0/1/2 号小目标。关键后处理步骤Sigmoid 激活apply_yolo每个锚框只对“前 2 个偏移量 tx/ty”2*w*h个值与“objectness 80 类概率”(1classes)*w*h个值做 logistic 激活宽高的 log 尺度保持未激活状态框解码get_yolo_boxL208-L217中心点(i tx)/lw, (j ty)/lh宽高exp(tw)*anchor_w / w、exp(th)*anchor_h / hletterbox 逆变换correct_yolo_boxL219-L234把 416×416 归一化坐标反推回原图坐标扣除灰边偏移并按缩放比例放大宽高阈值过滤get_yolo_detectionsobjectness 低于-th阈值默认 0.5的网格点直接丢弃类别概率取objectness × class_prob低于阈值的类置零NMSdo_nms_sortL300-L328逐类别按概率降序排序对 IoU 超过0.45detect()中硬编码传入的重复框清零可视化draw_detections每个检测框用“类别索引 → 6 色调色板插值”得到 RGB 颜色draw_box_width()按im.h * 0.006的线宽画空心矩形标签文字由data/labels/位图字体逐字符拼贴get_label()最终以 JPEG 质量 80 保存save_image(img, ..., 80)。entry_index()L60-L64体现了 YOLO 输出张量的内存布局anchor * w*h*(4classes1) entry*w*h loc即“按锚框分块、每块内含 4180 个连续特征平面”这也是255通道数的来源。结果解读与注意事项README 示例输出中 5 行“类别: 百分比”即draw_detections()的printf日志最后一行Detected objects saved in predictions.jpg (time: 0.057000 sec.)的耗时由ggml_time_ms()围绕detect()计时L646-L653包含 letterbox、图执行与全部后处理无 GPU 环境如未开启GGML_CUDA/GGML_METAL/GGML_VULKAN的纯 CPU 构建下程序会自动回退到 CPU 后端运行行为一致只是速度差异较大也可用-d显式指定设备、-t调整线程数默认取一半逻辑核-th阈值是召回/精度的主要调节旋钮调低如 0.3会输出更多低置信度框调高则更保守NMS 的 0.45 IoU 阈值与两头的锚框 mask 均在源码中固定属该示例的简化处理该示例模型分辨率固定为 416×416、类别固定为 COCO 80 类data/coco.names输入/输出文件名与模型名均可通过命令行覆盖但网络结构本身由build_graph()硬编码不打算支持其它 YOLO 变体。关键文件索引内容路径示例说明下载/转换/运行examples/yolo/README.md推理主程序模型加载、构图、后处理examples/yolo/yolov3-tiny.cppDarknet → GGUF 转换脚本examples/yolo/convert-yolov3-tiny.py图像 I/O、letterbox、绘制examples/yolo/yolo-image.cpp类别名 / 字体数据examples/yolo/data/coco.names、examples/yolo/data/labels/构建定义examples/yolo/CMakeLists.txt、examples/CMakeLists.txt相关核心库include/ggml.h、include/ggml-alloc.h、include/ggml-backend.h、include/gguf.h【免费下载链接】ggmlTensor library for machine learning项目地址: https://gitcode.com/GitHub_Trending/gg/ggml创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考