『7x24小时有问必答』

一、先说说这玩意儿是干嘛的

现在做图像分类,大家的第一反应是  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=16

struct  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. Softmax

qk = ggml_soft_max(ctx, qk);

// 5. 加权求和: Attn @ V

struct  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 → 3072

struct  ggml_tensor*  hidden  =  ggml_mul_mat(ctx,  w_fc1,  x);    // [3072, 197]

// GELU 激活

hidden = ggml_gelu(ctx, hidden);

// 降维: 3072 → 768

hidden = 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]

// Softmax

struct  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 timm

2. 转换模型权重

方式一:从 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.gguf

3. 编译

mkdir build &&  cd  build

cmake .. && make -j4

编译选项(可选,提升性能):
# 针对当前 CPU 架构优化(自动检测 AVX/SSE/NEON)

cmake .. -DCMAKE_CXX_FLAGS="-march=native"  && make -j4

# 开启 OpenMP 多线程

cmake .. -DCMAKE_CXX_FLAGS="-march=native -fopenmp"  && make -j4

4. 运行推理

# 基本用法

./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.8712

goldfish: 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 threadpoolctl

python scripts/benchmark.py

本项目基准:
chmod +x scripts/benchmark.sh

# FP16 模型

./scripts/benchmark.sh

# 量化模型(4 线程 + 量化)

./scripts/benchmark.sh 4 1

9. 常见问题

编译报错找不到 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【程序猿编码】

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码