一、先说说这玩意儿是干嘛的现在做图像分类大家的第一反应是pip install torch torchvision加载个预训练模型model(img)就完事了。这一套确实方便但代价是你的程序里突然多了几个G的依赖启动慢得像蜗牛内存占用高得吓人。一个 tiny 级别的 ViT 模型PyTorch 能吃掉将近 800MB 内存。这个项目干的事就是把这些全部抛掉。只用 C/C 和 ggml一个轻量张量计算库实现一个能在 CPU 上跑视觉 TransformerViT的推理引擎。它支持从 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 Encoder12 层堆叠每层 Norm Attention MLPViT 的核心计算MLP Head最后的分类层把 CLS token 的输出映射到 1000 个 ImageNet 类别ggml 计算图用 ggml 的图结构描述整个前向传播自动优化、支持量化、跨平台量化工具把 FP32/FP16 权重转成 q4_0/q5_1/q8_0省内存、加速推理多线程支持OpenMP ggml 线程池充分利用多核 CPU三、代码实现原理拆开来看1. 图像预处理从文件到张量推理的第一步是把图片文件读进来变成模型能处理的数字。流程很标准// 1. 读取图片用 stb_image单头文件库intwidth,height,channels;unsignedchar*img_datastbi_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(inti0;i224*224;i){floatr(img_data[i*30]/255.0f-0.485f)/0.229f;floatg(img_data[i*31]/255.0f-0.456f)/0.224f;floatb(img_data[i*32]/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 就是一个卷积层kernel16, stride16, out_channels768。卷积完之后你得到一个14 × 14 × 768的特征图展平一下变成196 × 768的序列——这就是 196 个 token每个 token 768 维。// ggml 里的 Patch Embedding 实现// 本质上是一个 2D 卷积kernel16, stride16structggml_tensor*patch_embedggml_conv_2d(ctx,// 计算图上下文w_patch,// 权重: [768, 3, 16, 16]img_tensor,// 输入: [3, 224, 224]16,16,// stride0,0,// padding1,1// dilation);// 输出: [768, 14, 14]// 然后 reshape 成 [196, 768]3. CLS Token Position Embedding切完 196 个 Patch token 之后还要做两件事加 CLS Token在序列最前面塞一个特殊的可学习向量。这个向量不参与 Patch 计算它的唯一作用就是——经过 12 层 Transformer 之后它的输出被拿去分类。你可以把它理解为班长负责汇总全班信息。加 Position Embedding196 个 Patch token 1 个 CLS token 197 个 token。但模型本身不知道哪个 token 在左上角、哪个在右下角所以需要给每个位置加一个可学习的向量注入位置信息。// CLS token: 一个可学习的 [1, 768] 向量structggml_tensor*cls_tokenmodel.cls_token;// [1, 768]// Position embedding: [197, 768] 的可学习矩阵structggml_tensor*pos_embedmodel.pos_embed;// [197, 768]// 拼接cls patch_tokens → [197, 768]structggml_tensor*tokensggml_concat(ctx,cls_token,patch_tokens);// 加位置编码tokensggml_add(ctx,tokens,pos_embed);4. Transformer Encoder12 层堆叠这是 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 投影structggml_tensor*qggml_mul_mat(ctx,w_q,x);// [768, 197]structggml_tensor*kggml_mul_mat(ctx,w_k,x);// [768, 197]structggml_tensor*vggml_mul_mat(ctx,w_v,x);// [768, 197]// 2. 分成多个头比如 12 头每头 64 维// reshape: [768, 197] → [12, 64, 197]qggml_reshape_3d(ctx,q,64,12,197);kggml_reshape_3d(ctx,k,64,12,197);vggml_reshape_3d(ctx,v,64,12,197);// 3. 算注意力分数: Q K^T / sqrt(d)structggml_tensor*qkggml_mul_mat(ctx,k,q);// [12, 197, 197]qkggml_scale(ctx,qk,1.0f/sqrtf(64.0f));// 4. Softmaxqkggml_soft_max(ctx,qk);// 5. 加权求和: Attn Vstructggml_tensor*attn_outggml_mul_mat(ctx,v,qk);// [12, 64, 197]// reshape 回 [768, 197]// 6. 输出投影attn_outggml_mul_mat(ctx,w_proj,attn_out);// [768, 197]MLP的实现// 升维: 768 → 3072structggml_tensor*hiddenggml_mul_mat(ctx,w_fc1,x);// [3072, 197]// GELU 激活hiddenggml_gelu(ctx,hidden);// 降维: 3072 → 768hiddenggml_mul_mat(ctx,w_fc2,hidden);// [768, 197]5. MLP Head从 CLS 到类别12 层 Transformer 跑完之后取第 0 个 token就是 CLS token的输出过一个线性层 Softmax得到 1000 个 ImageNet 类别的概率。// 取 CLS token 的输出: [768]structggml_tensor*cls_outputggml_view_1d(ctx,final_output,768,0);// MLP Head: [768] → [1000]structggml_tensor*logitsggml_mul_mat(ctx,w_head,cls_output);// [1000]// Softmaxstructggml_tensor*probsggml_soft_max(ctx,logits);// Top-k 排序输出概率最高的 5 个类别6. ggml 计算图把整个模型画成一张图ggml 的核心设计是计算图Computation Graph。你不是一行一行地执行运算而是先把整个前向传播的描述画成一张图然后一次性交给 ggml 去执行。这样做的好处自动优化ggml 可以在执行前做图优化比如算子融合、内存复用支持量化同一张图权重可以是 FP16、Q4_0、Q8_0计算逻辑不变跨平台ggml 底层自动适配 x86AVX/SSE、ARMNEON、WebAssembly// 构建计算图structggml_cgraph*gfggml_new_graph(ctx);// 把输出节点加到图里ggml_build_forward_expand(gf,probs);// 执行图ggml_graph_compute_with_ctx(ctx,gf,n_threads);7. 量化把模型压到 1/4 大小训练好的模型权重是 FP324 字节或 FP162 字节。量化就是把这些浮点数压缩成更小的整数表示。这个项目支持的量化格式格式每个权重占多少位说明q4_04.5每 32 个权重共享 1 个 scale精度最低体积最小q4_14.5每 32 个权重共享 1 个 scale 1 个 min精度稍好q5_05.5每 32 个权重共享 1 个 scale5 位表示q5_15.5每 32 个权重共享 1 个 scale 1 个 minq8_08.5每 32 个权重共享 1 个 scale8 位表示精度最好量化工具的使用# 把 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 HeadPatch Embedding卷积实现kernel16, stride16CLS Token可学习向量汇总全局信息用于分类Position Embedding可学习的位置编码替代正弦编码Multi-Head AttentionQ/K/V 投影 Scaled Dot-Product SoftmaxGELUMLP 的激活函数比 ReLU 平滑残差连接x f(x)防止梯度消失LayerNorm减均值、除标准差、乘权重、加偏置ggml 计算图描述计算流程支持优化和量化INT4/INT5/INT8 量化减少内存占用加速推理OpenMP 多线程充分利用多核 CPUstb_image单头文件图像加载库五、设计思路为什么这样设计设计选择为什么这么做PyTorch 会怎么做纯 C/C ggml零 Python 依赖启动快体积小Python PyTorch依赖重计算图模式一次构建多次执行支持优化Eager 模式逐行执行量化权重省内存、省带宽适合边缘设备通常 FP32/FP16量化需额外工具多线程 CPU不需要 GPU任何机器都能跑默认 CUDACPU 模式慢单可执行文件部署简单复制就能跑需要 Python 环境 依赖安装支持 timm 模型直接复用预训练权重同样支持但加载方式不同六、代码实现用途边缘设备部署树莓派、嵌入式板子内存和算力都有限Serverless 推理AWS Lambda、Cloudflare Workers冷启动时间敏感学习 ViT 原理没有框架封装每个 Patch、每层 Attention 都看得见模型量化研究ggml 支持多种量化格式方便对比效果跨平台移植纯 C/CWindows/Linux/macOS 都能编译七、流程原理图图1ViT 整体推理流程图2图像切 Patch 过程图3Transformer Encoder 内部结构图4量化效果与性能对比八、怎么跑起来一步步来1. 安装 Python 依赖用于转换模型pipinstalltorch timm2. 转换模型权重方式一从 timm 下载并转换# 列出所有支持的模型python convert-pth-to-ggml.py--list# 转换一个 tiny 模型Patch16, 图像尺寸224, ImageNet21k预训练ImageNet1k微调python convert-pth-to-ggml.py\--model_namevit_tiny_patch16_224.augreg_in21k_ft_in1k\--ftype1--ftype参数0 FP321 FP16推荐默认转换完成后会生成ggml-model-f16.gguf。方式二直接下载已转换的权重wgethttps://huggingface.co/staghado/vit.cpp/blob/main/tiny-ggml-model-f16.gguf3. 编译mkdirbuildcdbuild cmake..make-j4编译选项可选提升性能# 针对当前 CPU 架构优化自动检测 AVX/SSE/NEONcmake..-DCMAKE_CXX_FLAGS-marchnativemake-j4# 开启 OpenMP 多线程cmake..-DCMAKE_CXX_FLAGS-marchnative -fopenmpmake-j44. 运行推理# 基本用法./bin/vit-t4-m../ggml-model-f16.gguf-i../assets/tench.jpg# 参数说明# -t 4 用 4 个线程# -m ... 模型文件路径# -i ... 输入图片路径# -k 5 输出 Top-5 分类结果默认# -s 42 随机种子默认 -1即随机输出示例tench: 0.8712 goldfish: 0.0523 ...5. 量化模型# 先转成 FP32如果还没转的话python convert-pth-to-ggml.py--model_name...--ftype0# 量化成 q5_1./bin/quantize\../ggml-model-f32.gguf\../ggml-model-q5_1.gguf\7# 用量化模型推理./bin/vit-t4-m../ggml-model-q5_1.gguf-i../assets/tench.jpg量化类型对照2 q4_03 q4_16 q5_07 q5_18 q8_08. 跑基准测试PyTorch 基准pipinstallmemory_profiler threadpoolctl python scripts/benchmark.py本项目基准chmodx scripts/benchmark.sh# FP16 模型./scripts/benchmark.sh# 量化模型4 线程 量化./scripts/benchmark.sh419. 常见问题编译报错找不到 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【程序猿编码】