// AVX2 优化的 INT8 × FP32 矩阵乘法__m256 acc = _mm256_setzero_ps(); // 8个float的累加器for (int i = 0; i < dim; i += 8) { // 加载8个FP32激活值 __m256 x_vec = _mm256_loadu_ps(x + i); // 加载8个INT8权重,扩展成INT16,再扩展成FP32 __m128i w_i8 = _mm_loadu_si64(w + i); // 8个int8 __m256i w_i16 = _mm256_cvtepi8_epi16(w_i8); // 扩展成16个int16(只用低8个) __m256 w_f32 = _mm256_cvtepi32_ps(_mm256_cvtepi16_epi32(w_i16)); // 扩展成8个float // 反量化:w_f32 * scale w_f32 = _mm256_mul_ps(w_f32, _mm256_set1_ps(scale)); // FMA: acc += x_vec * w_f32 acc = _mm256_fmadd_ps(x_vec, w_f32, acc);}// 水平求和:把8个float加起来float result = hsum256_ps(acc);_mm256_fmadd_ps是FMA(Fused Multiply-Add)指令,一条指令完成a*b+c,比先乘后加少一次舍入误差,而且延迟更低。
3. Gemma 3的模型结构
Gemma 3是Google开源的一系列轻量级语言模型。这个项目支持的两个版本:
参数 | 270M | 1B |
层数 | 26 | 28 |
注意力头数 | 4 | 4 |
KV头数(GQA) | 1 | 1 |
嵌入维度 | 1536 | 2048 |
MLP中间维度 | 6144 | 8192 |
上下文长度 | 128K | 128K |
Gemma 3有几个值得注意的设计:
可学习的位置偏置:不像GPT-2用固定的正弦位置编码,Gemma的Embedding层里有一个可学习的embed_positions参数,直接加到token嵌入上。
GQA(Grouped Query Attention):4个Query头共享1个K头和1个V头。这样KV Cache的内存只需要原来的1/4,解码时加载K/V的带宽也少了3/4。
GeGLU激活:MLP不是传统的up_proj → GELU → down_proj,而是gate_proj × SiLU(up_proj) → down_proj。门控机制让模型能选择性激活不同的神经元。
RMSNorm:和LayerNorm的区别是不减均值,直接除以均方根。公式是:output = x / sqrt(mean(x²) + eps) * weight。
4. Min-P采样:小模型的最佳采样策略
传统的采样方法有Greedy(贪心)、Top-K(保留前K个)、Top-P(核采样)。这个项目实现的是Min-P,它对小模型特别有效。
Min-P的核心思想是:先找到概率最高的token,然后只保留概率超过max_prob × p_threshold的token。比如最高概率是0.3,p_threshold是0.1,那阈值就是0.03,所有概率低于0.03的token直接扔掉。
// Min-P 采样逻辑float max_prob = -INFINITY;for (int i = 0; i < vocab_size; i++) { if (probs > max_prob) max_prob = probs;}float threshold = max_prob * min_p;int num_candidates = 0;for (int i = 0; i < vocab_size; i++) { if (probs >= threshold) { candidates[num_candidates++] = i; }}// 在候选集里按temperature重新采样// temperature越低,越倾向于选概率高的Min-P的好处是:它不会死板地只保留固定数量的token(Top-K的问题),也不会因为长尾分布而保留太多垃圾token(Top-P的问题)。对小模型来说,Min-P能在"多样性"和"质量"之间找到更好的平衡。
5. KV Cache:解码速度的生命线
自回归生成时,每个新token都需要看前面所有token的K和V。如果没有Cache,每步都要重新算一遍,时间随长度平方增长。
这个项目的KV Cache实现很直接:每层预先分配一块足够大的内存,生成时把新token的K/V追加到末尾。
// KV Cache 结构(简化示意)struct KVCache { float* k; // [n_layers, max_seq_len, n_kv_heads, head_dim] float* v; // 同上 int seq_len; // 当前已缓存的长度};// 推理时void forward(KVCache& cache, int pos, ...) { // 只算当前位置pos的K和V compute_k(q_proj_output, cache.k + pos * kv_stride); compute_v(k_proj_output, cache.v + pos * kv_stride); // Attention计算时,从cache里读全部历史K/V for (int t = 0; t <= pos; t++) { float* k_t = cache.k + t * kv_stride; float* v_t = cache.v + t * kv_stride; // 算注意力分数... }}GQA让KV Cache的内存需求大幅降低。以270M模型为例:26层、1个KV头、head_dim=384、max_seq_len=128K,总内存大约是26 × 128000 × 1 × 384 × 2(K+V) × 4字节 ≈ 10GB。实际运行时不会用到128K,通常几百到几千token就够了。
四、相关领域知识点
知识点 | 在这项目里的体现 |
| INT8量化 | 权重存INT8,计算时反量化,省75%内存 |
| AVX2 SIMD | 256位寄存器,一次处理8个FP32或32个INT8 |
| FMA指令 | _mm256_fmadd_ps ,乘加融合,减少延迟 |
| GQA | 4个Query共享1个KV头,省3/4 KV Cache内存 |
| RoPE | 旋转位置编码,二维旋转注入位置信息 |
| RMSNorm | 不减均值,直接除以均方根 |
| GeGLU | 门控线性单元,SiLU作门控信号 |
| Min-P采样 | 动态阈值,比Top-K/Top-P更适合小模型 |
| INT8反量化技巧 | (x·w_int8)×scale 减少浮点运算 |
| 单线程优化 | 没有线程同步开销,Cache利用率更高 |
五、设计思路:为什么这样设计?
设计选择 | 为什么这么做 | 生产框架会怎么做 |
单线程 | 没有锁竞争,Cache命中率高,代码简单 | 多线程+线程池,调度复杂 |
INT8权重量化 | 省内存、省带宽,CPU Cache能装更多数据 | FP16/INT4,更复杂但更快 |
AVX2手写内核 | 不依赖编译器自动向量化,可控可测 | 调OpenBLAS/MKL,黑盒 |
零外部依赖 | 一个Makefile搞定,任何人都能编译 | CMake+Conda+vcpkg,环境地狱 |
C++20 | std::span 、concept等新特性让代码更简洁 | C++11/14兼容旧编译器 |
Min-P采样 | 小模型上效果比Top-K/Top-P更好 | 通常实现Top-K+Top-P组合 |
逐层单元测试 | 跟NumPy参考实现对比,确保正确性 | 端到端测试,定位问题难 |
这些选择的叠加效果就是:代码量小、编译简单、运行飞快。270M模型单线程解码58.8 token/秒,意味着你输入一个问题,模型每秒能吐出将近60个字,完全够用。
六、代码实现用途
学习INT8量化和AVX2优化:手写代码比看框架源码直观100倍
理解Gemma 3的模型结构:GQA、GeGLU、可学习位置偏置,全部自己实现
作为嵌入式/边缘设备的起点:纯C++、单线程、无依赖,移植极简单
性能基准测试:单线程CPU推理的上限参考
教学演示:从量化到采样,完整链路一手掌握
七、流程原理图
图1:整体架构
If you need the complete source code, please add the WeChat number (c17865354792)
图2:INT8权重量化
图3:性能对比
八、怎么跑起来?一步步来
1. 安装Python依赖
pip install -r requirements.txt2. 下载模型权重
需要先登录HuggingFace(需要token):
huggingface-cli login# 或者hf auth login# 下载270M指令模型的tokenizer和权重hf download google/gemma-3-270m-it tokenizer.model --local-dir weightshf download google/gemma-3-270m-it model.safetensors --local-dir weights4. 转换权重和tokenizer
# 把safetensors转成INT8二进制格式python3 -m py.convert \ --weights-in-path 'weights/model.safetensors' \ --weights-out-path 'weights/gemma_i8.bin' \ --model-size '270m' \ --tok-in-path 'weights/tokenizer.model' \ --tok-out-path 'weights/tokenizer_gemma3.bin'转换完成后,weights/目录下会有:
gemma_i8.bin:INT8量化后的模型权重
tokenizer_gemma3.bin:二进制格式的tokenizer
5. 编译
make gemma要求:
GCC 10+ 或 Clang(支持C++20)
CPU支持AVX2(2013年之后的Intel/AMD CPU基本都支持)
6. 运行推理
带Min-P采样的对话模式:
./build/gemma \ --weights_path weights/gemma_i8.bin \ --model_size "270m" \ --n_dec 250 \ --minp 0.1 \ --temp 0.7 \ --prompt "What is a transformer?" \ --terminate_on_eos 1 \ --chat_format 1参数说明:
--weights_path:模型权重文件路径
--model_size:模型大小(270m或1B)
--n_dec:最多生成多少个token
--minp:Min-P采样阈值(0~1,越小越保守)
--temp:温度(越低越确定,越高越随机)
--prompt:输入提示
--terminate_on_eos:遇到结束符就停止
--chat_format:使用Gemma的对话格式
贪心解码(最确定的输出):
./build/gemma \ --weights_path weights/gemma_i8_1B.bin \ --model_size "1B" \ --n_dec 250 \ --prompt "What is one difference between GPT2 and BERT?" \ --terminate_on_eos 1 \ --chat_format 1不加--minp就是贪心解码,每次都选概率最高的token。
7. 跑Passkey测试(长上下文)
make passkey./build/passkey \ --weights_path weights/gemma_i8.bin \ --model_size "270m" \ --n_garbage 16384这个测试会在prompt里塞16384个随机字符,然后在中间藏一个"密码",看模型能不能在生成时正确回忆出来。用来验证长上下文能力。
注意:长prompt在单线程CPU上处理很慢,可能需要几分钟。
8. 跑单元测试
# 编译测试make unittests# 生成参考数据(用NumPy实现跑一遍,保存中间结果)./tests/scripts/unittests/create_test_data.sh# 运行测试./tests/scripts/unittests/run_tests.sh单元测试会把C++的每一层输出跟NumPy参考实现对比,确保量化、矩阵乘法、注意力、MLP等每个模块都算对了。
集成测试(需要下载完整的safetensors权重):
make integrations./tests/scripts/integrations/create_test_weights_logits.sh./tests/scripts/integrations/run_test.sh集成测试跑255步贪心解码,对比C++和Python输出的token序列和logits分布。
9. 常见问题
编译报错找不到AVX2:检查CPU是否支持AVX2(cat /proc/cpuinfo | grep avx2),不支持的话这个项目跑不了
hf download失败:检查HuggingFace登录状态和token权限,Gemma模型需要同意使用协议
内存不足:270M模型需要约300MB内存,1B模型需要约1GB内存
生成速度很慢:确认编译时开了优化(make会自动加-O3 -march=native),检查是否在用Debug模式
九、总结
这个项目最大的价值,是让你看到CPU单线程推理可以有多快。
它用纯C++20实现了:
INT8权重量化(省75%内存)
AVX2向量化矩阵乘法(一次算8个FP32)
Gemma 3的完整模型结构(GQA、GeGLU、RMSNorm、RoPE)
Min-P采样策略
逐层单元测试确保正确性
而且全部代码加起来可能就几千行,没有复杂的构建系统,没有依赖地狱,一个make就能编译,一条命令就能运行。
270M模型单线程58.8 token/秒的解码速度,意味着你在一个普通的办公笔记本上,就能流畅地跟Gemma 3对话。如果你想理解"CPU推理是怎么优化的",或者需要一个轻量、可移植、无依赖的推理方案,这个项目就是最好的起点。把代码通读一遍,亲手改改量化精度、采样温度、模型大小,你会对INT8量化和AVX2优化有一个完全不一样的体感。
Welcome to follow WeChat official account【程序猿编码】
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!