一、先说说这玩意儿是干嘛的 现在做图像分类,大家的第一反应是 pip install torch torchvision,加载个预训练模型,model(img) 就完事了。这一套确实方便,但代价是:你的程序里突然多了几个G的依赖,启动慢得像蜗牛,内存占用高得吓人。一个 tiny 级别的 ViT 模型,PyTorch 能吃掉将近 800MB 内存。 这个项目干的事,就是把这些全部抛掉。只用 C/C++ 和 ggml(一个轻量张量计算库),实现一个能在 CPU 上跑视觉 Transformer(ViT)的推理引擎。它支持从 timm 下载的各种 ViT 变体,还能把模型量化到 4-bit、5-bit、8-bit,内存占用直接砍到 PyTorch 的 1/39。 最狠的是启动速度。传统深度学习框架冷启动要加载一大堆动态库,这个项目编译出来就一个可执行文件,双击就能跑。对于 serverless 部署(比如 AWS Lambda、Cloudflare Workers)来说,冷启动时间直接决定用户体验,这个项目在这方面有天然优势。 二、具体含义:它到底包含哪些东西? 模块 | 干啥的 | 为什么重要 | | 图像预处理 | 读取图片、缩放、归一化、切 Patch | 把 JPEG/PNG 变成模型能吃的格式 | | Patch Embedding | 用卷积把 16×16 的图像块投影成向量 | ViT 的核心输入转换 | | Position Embedding | 给每个 Patch 加上位置信息 | 让模型知道"左上角"和"右下角"的区别 | | CLS Token | 一个可学习的分类令牌 | 最终分类结果从这个 token 的输出拿 | | Transformer Encoder | 12 层堆叠,每层 = Norm + Attention + MLP | ViT 的核心计算 | | MLP Head | 最后的分类层 | 把 CLS token 的输出映射到 1000 个 ImageNet 类别 | | ggml 计算图 | 用 ggml 的图结构描述整个前向传播 | 自动优化、支持量化、跨平台 | | 量化工具 | 把 FP32/FP16 权重转成 q4_0/q5_1/q8_0 | 省内存、加速推理 | | 多线程支持 | OpenMP + ggml 线程池 | 充分利用多核 CPU |
三、代码实现原理:拆开来看 1. 图像预处理:从文件到张量 推理的第一步是把图片文件读进来,变成模型能处理的数字。流程很标准: // 1. 读取图片(用 stb_image,单头文件库)int width, height, channels;unsigned char* img_data = stbi_load(image_path, &width, &height, &channels, 3);// 2. 缩放 + 中心裁剪到 224×224// 先等比缩放,让短边变成 224,然后从中间裁一块 224×224 出来// 3. 归一化:减去 ImageNet 均值,除以标准差// mean = [0.485, 0.456, 0.406], std = [0.229, 0.224, 0.225]for (int i = 0; i < 224 * 224; i++) { float r = (img_data[i * 3 + 0] / 255.0f - 0.485f) / 0.229f; float g = (img_data[i * 3 + 1] / 255.0f - 0.456f) / 0.224f; float b = (img_data[i * 3 + 2] / 255.0f - 0.406f) / 0.225f; // 存到 ggml 张量里...}2. Patch Embedding:把图像切成"词" 这是 ViT 最反直觉的地方。传统 CNN 是一层一层卷积、池化,ViT 的做法是:把图像切成固定大小的小块,每个小块当成一个"词"(token)。 假设输入是 224×224 的图,Patch 大小是 16×16: 横向能切 224 / 16 = 14 块 纵向也能切 14 块 总共 14 × 14 = 196 个 Patch 每个 Patch 是 16 × 16 × 3 = 768 个像素值。Patch Embedding 就是一个卷积层:kernel=16, stride=16, out_channels=768。卷积完之后,你得到一个 14 × 14 × 768 的特征图,展平一下变成 196 × 768 的序列——这就是 196 个 token,每个 token 768 维。 // ggml 里的 Patch Embedding 实现// 本质上是一个 2D 卷积,kernel=16, stride=16struct ggml_tensor* patch_embed = ggml_conv_2d( ctx, // 计算图上下文 w_patch, // 权重: [768, 3, 16, 16] img_tensor, // 输入: [3, 224, 224] 16, 16, // stride 0, 0, // padding 1, 1 // dilation);// 输出: [768, 14, 14]// 然后 reshape 成 [196, 768]3. CLS Token + Position Embedding 切完 196 个 Patch token 之后,还要做两件事: 加 CLS Token:在序列最前面塞一个特殊的可学习向量。这个向量不参与 Patch 计算,它的唯一作用就是——经过 12 层 Transformer 之后,它的输出被拿去分类。你可以把它理解为"班长",负责汇总全班信息。 加 Position Embedding:196 个 Patch token + 1 个 CLS token = 197 个 token。但模型本身不知道哪个 token 在左上角、哪个在右下角,所以需要给每个位置加一个可学习的向量,注入位置信息。 // CLS token: 一个可学习的 [1, 768] 向量struct ggml_tensor* cls_token = model.cls_token; // [1, 768]// Position embedding: [197, 768] 的可学习矩阵struct ggml_tensor* pos_embed = model.pos_embed; // [197, 768]// 拼接:cls + patch_tokens → [197, 768]struct ggml_tensor* tokens = ggml_concat(ctx, cls_token, patch_tokens);// 加位置编码tokens = ggml_add(ctx, tokens, pos_embed);4. Transformer Encoder:12 层堆叠 这是 ViT 的核心。每层 Encoder 的结构是: 输入 x→ LayerNorm(x)→ Multi-Head Self-Attention(x)→ x + Attention(x) (残差连接)→ LayerNorm(x)→ MLP(x) (升维→GELU→降维)→ x + MLP(x) (残差连接)→ 输出重复 12 次。 Multi-Head Attention 的实现: // 输入 x: [197, 768]// 1. Q/K/V 投影struct ggml_tensor* q = ggml_mul_mat(ctx, w_q, x); // [768, 197]struct ggml_tensor* k = ggml_mul_mat(ctx, w_k, x); // [768, 197]struct ggml_tensor* v = ggml_mul_mat(ctx, w_v, x); // [768, 197]// 2. 分成多个头(比如 12 头,每头 64 维)// reshape: [768, 197] → [12, 64, 197]q = ggml_reshape_3d(ctx, q, 64, 12, 197);k = ggml_reshape_3d(ctx, k, 64, 12, 197);v = ggml_reshape_3d(ctx, v, 64, 12, 197);// 3. 算注意力分数: Q @ K^T / sqrt(d)struct ggml_tensor* qk = ggml_mul_mat(ctx, k, q); // [12, 197, 197]qk = ggml_scale(ctx, qk, 1.0f / sqrtf(64.0f));// 4. Softmaxqk = ggml_soft_max(ctx, qk);// 5. 加权求和: Attn @ Vstruct ggml_tensor* attn_out = ggml_mul_mat(ctx, v, qk); // [12, 64, 197]// reshape 回 [768, 197]// 6. 输出投影attn_out = ggml_mul_mat(ctx, w_proj, attn_out); // [768, 197]MLP 的实现: // 升维: 768 → 3072struct ggml_tensor* hidden = ggml_mul_mat(ctx, w_fc1, x); // [3072, 197]// GELU 激活hidden = ggml_gelu(ctx, hidden);// 降维: 3072 → 768hidden = ggml_mul_mat(ctx, w_fc2, hidden); // [768, 197]5. MLP Head:从 CLS 到类别 12 层 Transformer 跑完之后,取第 0 个 token(就是 CLS token)的输出,过一个线性层 + Softmax,得到 1000 个 ImageNet 类别的概率。 // 取 CLS token 的输出: [768]struct ggml_tensor* cls_output = ggml_view_1d(ctx, final_output, 768, 0);// MLP Head: [768] → [1000]struct ggml_tensor* logits = ggml_mul_mat(ctx, w_head, cls_output); // [1000]// Softmaxstruct ggml_tensor* probs = ggml_soft_max(ctx, logits);// Top-k 排序,输出概率最高的 5 个类别6. ggml 计算图:把整个模型"画"成一张图 ggml 的核心设计是计算图(Computation Graph)。你不是一行一行地执行运算,而是先把整个前向传播的描述"画"成一张图,然后一次性交给 ggml 去执行。 这样做的好处: 自动优化:ggml 可以在执行前做图优化(比如算子融合、内存复用) 支持量化:同一张图,权重可以是 FP16、Q4_0、Q8_0,计算逻辑不变 跨平台:ggml 底层自动适配 x86(AVX/SSE)、ARM(NEON)、WebAssembly // 构建计算图struct ggml_cgraph* gf = ggml_new_graph(ctx);// 把输出节点加到图里ggml_build_forward_expand(gf, probs);// 执行图ggml_graph_compute_with_ctx(ctx, gf, n_threads);7. 量化:把模型压到 1/4 大小 训练好的模型权重是 FP32(4 字节)或 FP16(2 字节)。量化就是把这些浮点数压缩成更小的整数表示。 这个项目支持的量化格式: 格式 | 每个权重占多少位 | 说明 | q4_0 | 4.5 | 每 32 个权重共享 1 个 scale,精度最低,体积最小 | q4_1 | 4.5 | 每 32 个权重共享 1 个 scale + 1 个 min,精度稍好 | q5_0 | 5.5 | 每 32 个权重共享 1 个 scale,5 位表示 | q5_1 | 5.5 | 每 32 个权重共享 1 个 scale + 1 个 min | q8_0 | 8.5 | 每 32 个权重共享 1 个 scale,8 位表示,精度最好 |
量化工具的使用: # 把 FP16 模型量化成 q5_1./bin/quantize \ ../tiny-ggml-model-f16.gguf \ ../tiny-ggml-model-f16-quant.gguf \ 7# 7 = q5_1量化后的模型文件可以直接拿来推理,ggml 会在计算时自动反量化。 四、相关领域知识点 知识点 | 在这项目里的体现 | | ViT 架构 | Patch Embedding + Transformer Encoder + MLP Head | | Patch Embedding | 卷积实现,kernel=16, stride=16 | | CLS Token | 可学习向量,汇总全局信息用于分类 | | Position Embedding | 可学习的位置编码,替代正弦编码 | | Multi-Head Attention | Q/K/V 投影 + Scaled Dot-Product + Softmax | | GELU | MLP 的激活函数,比 ReLU 平滑 | | 残差连接 | x + f(x) ,防止梯度消失 | | LayerNorm | 减均值、除标准差、乘权重、加偏置 | | ggml 计算图 | 描述计算流程,支持优化和量化 | | INT4/INT5/INT8 量化 | 减少内存占用,加速推理 | | OpenMP 多线程 | 充分利用多核 CPU | | stb_image | 单头文件图像加载库 |
五、设计思路:为什么这样设计? 设计选择 | 为什么这么做 | PyTorch 会怎么做 | 纯 C/C++ + ggml | 零 Python 依赖,启动快,体积小 | Python + PyTorch,依赖重 | 计算图模式 | 一次构建,多次执行,支持优化 | Eager 模式,逐行执行 | 量化权重 | 省内存、省带宽,适合边缘设备 | 通常 FP32/FP16,量化需额外工具 | 多线程 CPU | 不需要 GPU,任何机器都能跑 | 默认 CUDA,CPU 模式慢 | 单可执行文件 | 部署简单,复制就能跑 | 需要 Python 环境 + 依赖安装 | 支持 timm 模型 | 直接复用预训练权重 | 同样支持,但加载方式不同 |
六、代码实现用途 边缘设备部署:树莓派、嵌入式板子,内存和算力都有限 Serverless 推理:AWS Lambda、Cloudflare Workers,冷启动时间敏感 学习 ViT 原理:没有框架封装,每个 Patch、每层 Attention 都看得见 模型量化研究:ggml 支持多种量化格式,方便对比效果 跨平台移植:纯 C/C++,Windows/Linux/macOS 都能编译 七、流程原理图 图1:ViT 整体推理流程
图2:图像切 Patch 过程
图3:Transformer Encoder 内部结构
图4:量化效果与性能对比
八、怎么跑起来?一步步来 1. 安装 Python 依赖(用于转换模型) pip install torch timm2. 转换模型权重 方式一:从 timm 下载并转换 # 列出所有支持的模型python convert-pth-to-ggml.py --list# 转换一个 tiny 模型(Patch=16, 图像尺寸=224, ImageNet21k预训练+ImageNet1k微调)python convert-pth-to-ggml.py \ --model_name vit_tiny_patch16_224.augreg_in21k_ft_in1k \ --ftype 1--ftype 参数: 0 = FP32 1 = FP16(推荐,默认) 转换完成后会生成 ggml-model-f16.gguf。 方式二:直接下载已转换的权重 wget https://huggingface.co/staghado/vit.cpp/blob/main/tiny-ggml-model-f16.gguf3. 编译 mkdir build && cd buildcmake .. && make -j4编译选项(可选,提升性能): # 针对当前 CPU 架构优化(自动检测 AVX/SSE/NEON)cmake .. -DCMAKE_CXX_FLAGS="-march=native" && make -j4# 开启 OpenMP 多线程cmake .. -DCMAKE_CXX_FLAGS="-march=native -fopenmp" && make -j44. 运行推理 # 基本用法./bin/vit -t 4 -m ../ggml-model-f16.gguf -i ../assets/tench.jpg# 参数说明# -t 4 用 4 个线程# -m ... 模型文件路径# -i ... 输入图片路径# -k 5 输出 Top-5 分类结果(默认)# -s 42 随机种子(默认 -1,即随机)输出示例: tench: 0.8712goldfish: 0.0523...5. 量化模型 # 先转成 FP32(如果还没转的话)python convert-pth-to-ggml.py --model_name ... --ftype 0# 量化成 q5_1./bin/quantize \ ../ggml-model-f32.gguf \ ../ggml-model-q5_1.gguf \ 7# 用量化模型推理./bin/vit -t 4 -m ../ggml-model-q5_1.gguf -i ../assets/tench.jpg量化类型对照: 2 = q4_0 3 = q4_1 6 = q5_0 7 = q5_1 8 = q8_0 8. 跑基准测试 PyTorch 基准: pip install memory_profiler threadpoolctlpython scripts/benchmark.py本项目基准: chmod +x scripts/benchmark.sh# FP16 模型./scripts/benchmark.sh# 量化模型(4 线程 + 量化)./scripts/benchmark.sh 4 19. 常见问题 编译报错找不到 ggml:--recurse-submodules 很重要,因为 ggml 是作为子模块引入的 模型转换失败:检查 timm 是否安装,pip install timm 推理结果不对:确认图片是 RGB 格式,且经过了正确的归一化 线程数不是越多越好:通常等于物理核心数最佳,超线程不一定有帮助 AMD CPU 优化:可以尝试 AOCC 编译器,但收益因型号而异 九、总结 这个项目最大的价值,是让你看到一个"能跑"的视觉 Transformer 可以有多小、多快、多简单。 它用纯 C/C++ + ggml 实现了: 完整的 ViT 推理链路(图像读取 → Patch Embedding → 12层 Transformer → 分类) 多种量化格式(q4_0 到 q8_0) 多线程 CPU 加速 跨平台支持(x86/ARM/WebAssembly) 而且 tiny 模型只占 20MB 内存,比 PyTorch 少了 39 倍;推理速度 120ms,比 PyTorch 快了 3.6 倍。在 Apple M1 上 base 模型甚至能快 6 倍。 如果你想真正理解"ViT 是怎么把一张图变成分类结果的",或者需要一个轻量、快速、无依赖的图像分类方案,这个项目就是最好的起点。把代码通读一遍,亲手转个模型、调调线程数、试试不同量化格式,你会对视觉 Transformer 的推理有一个完全不一样的体感。 If you need the complete source code, please add the WeChat number (c17865354792) Welcome to follow WeChat official account【程序猿编码】 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |