
前言
大家好,我是Dream。转眼间已经研二了,本科学的计算机,说实话当时C++学得挺痛苦的。指针、内存管理、模板这些东西,考试前临时抱佛脚勉强过了。真正让我重新拾起C++,是因为今年上半年做课题需要跑大模型,导师给的服务器GPU不够用,我就想着能不能优化一下推理速度。这一折腾,反而让我发现了C++在AI时代的价值。不夸张地说,现在主流的LLM推理框架,底层基本都是C++写的。作为一个研究生,我觉得这个方向挺值得深入的,就把自己这几个月的摸索记录下来。
大二学C++那会儿,最头疼的就是指针和内存管理。我记得有次写链表作业,程序老是莫名其妙崩溃,调试了一整晚才发现是忘了初始化指针。那时候就想,要是有Python那么简单就好了。
后来读研,课题组主要用Python做实验,我也就很少碰C++了。直到今年3月份,我在跑一个文本生成任务,用的Hugging Face的transformers库。模型不大,7B参数,但推理速度慢得离谱,生成一句话要好几秒。
导师说GPU资源紧张,让我想办法优化。我就开始到处找资料,发现了llama.cpp这个项目。这玩意儿挺神奇的,纯CPU跑大模型,速度比我用PyTorch快好几倍。仔细一看,全是C++写的。
先说说我的理解。现在大家谈AI,基本都在用Python。PyTorch、TensorFlow这些框架确实方便,几行代码就能搭个模型。但深入看,这些框架的底层实现,全是C++。
比如PyTorch,你写的Python代码最后会调用到LibTorch,这是个C++库。为什么要这么设计?因为性能。LLM 推理最核心的工作是矩阵运算,尤其是大规模矩阵乘法。一个 7B 参数的模型,每生成一个 token,就得完成几十亿次浮点运算。这种计算量极大的任务,Python 根本扛不住,必须靠 C++ 这种更贴近硬件的语言做深度优化。我做过个简单测试:同样的矩阵乘法,Python 用 numpy 跑要 50 毫秒,直接用 C++ 写只要 5 毫秒;要是再加上 SIMD 指令优化,能压到 2 毫秒以内。这 10 倍甚至 20 倍的速度差距,放到实际应用里,就是真金白银的成本差异。更关键的是,做推理服务的时候,不可能每个用户请求都等好几秒。工业界要求P99延迟控制在几十毫秒,这就必须用C++来榨干硬件性能。
看了一堆资料后,我决定自己动手试试。第一步就是实现一个高效的矩阵乘法,这是LLM推理的基础操作。
先写个最朴素的版本:
#include <iostream> #include <vector> #include <chrono> #include <random> using namespace std; // 朴素的矩阵乘法 void matmul_naive(const vector<vector<float>>& A, const vector<vector<float>>& B, vector<vector<float>>& C) { int M = A.size(); int K = A[0].size(); int N = B[0].size(); for (int i = 0; i < M; i++) { for (int j = 0; j < N; j++) { float sum = 0.0f; for (int k = 0; k < K; k++) { sum += A[i][k] * B[k][j]; } C[i][j] = sum; } } } // 测试函数 void test_matmul() { int M = 512, K = 512, N = 512; // 初始化矩阵 vector<vector<float>> A(M, vector<float>(K)); vector<vector<float>> B(K, vector<float>(N)); vector<vector<float>> C(M, vector<float>(N, 0.0f)); random_device rd; mt19937 gen(rd()); uniform_real_distribution<> dis(0.0, 1.0); for (int i = 0; i < M; i++) for (int j = 0; j < K; j++) A[i][j] = dis(gen); for (int i = 0; i < K; i++) for (int j = 0; j < N; j++) B[i][j] = dis(gen); // 计时 auto start = chrono::high_resolution_clock::now(); matmul_naive(A, B, C); auto end = chrono::high_resolution_clock::now(); auto duration = chrono::duration_cast<chrono::milliseconds>(end - start); } int main() { test_matmul(); return 0; }
编译运行:
g++ -O2 -o matmul matmul.cpp ./matmul
运行结果展示:

null
我的笔记本跑出来大概2000ms!这个速度对于LLM来说完全不够用。
然后我查资料学习了缓存优化技术,改进了一下:
// 分块矩阵乘法,提高缓存命中率 void matmul_blocked(const vector<vector<float>>& A, const vector<vector<float>>& B, vector<vector<float>>& C) { int M = A.size(); int K = A[0].size(); int N = B[0].size(); int block_size = 64; // 分块大小 for (int i0 = 0; i0 < M; i0 += block_size) { for (int j0 = 0; j0 < N; j0 += block_size) { for (int k0 = 0; k0 < K; k0 += block_size) { // 处理一个块 int i_end = min(i0 + block_size, M); int j_end = min(j0 + block_size, N); int k_end = min(k0 + block_size, K); for (int i = i0; i < i_end; i++) { for (int j = j0; j < j_end; j++) { float sum = C[i][j]; for (int k = k0; k < k_end; k++) { sum += A[i][k] * B[k][j]; } C[i][j] = sum; } } } } } }
这个版本利用了CPU缓存的局部性原理,把大矩阵分成小块处理。同样的数据,耗时降到了120ms左右,快了一倍多。
但这还不够。真正的LLM推理引擎会用到更多技术,比如:
这些优化叠加起来,能让性能再提升5-10倍。
搞明白矩阵乘法后,llama.cpp 确实是个很适合深入学习 LLM 推理底层逻辑的项目,它把复杂的模型推理流程拆解得很清晰,从权重加载、token 处理到每一层的矩阵运算、激活函数计算,都能在代码里找到对应实现。
核心的推理逻辑在llama.cpp文件里,我挑几个关键点说说:
LLM模型动辄几个GB,llama.cpp用了GGUF格式存储,支持多种量化方案。量化的意思是把float32的权重转成int8或者int4,模型大小能缩小4-8倍。
// 简化的量化示例 struct QuantizedWeight { vector<int8_t> data; // 量化后的数据 float scale; // 缩放因子 float zero_point; // 零点 }; // 量化过程 void quantize(const vector<float>& weights, QuantizedWeight& qweight) { float min_val = *min_element(weights.begin(), weights.end()); float max_val = *max_element(weights.begin(), weights.end()); qweight.scale = (max_val - min_val) / 255.0f; qweight.zero_point = min_val; qweight.data.resize(weights.size()); for (size_t i = 0; i < weights.size(); i++) { float normalized = (weights[i] - min_val) / qweight.scale; qweight.data[i] = static_cast<int8_t>(round(normalized)); } } // 反量化 float dequantize(int8_t value, float scale, float zero_point) { return value * scale + zero_point; }
量化虽然会损失一点精度,但在大多数任务上影响不大,换来的是更快的速度和更小的内存占用。这对于在普通电脑上跑模型特别重要。
Transformer的核心是Self-Attention,也就是Attention(Q, K, V),如下面的图所示:

null
看起来简单,但实现起来有很多细节。llama.cpp里用了一些技巧,比如Flash Attention的思想,减少中间结果的内存占用。
// 简化的attention计算 void compute_attention(const vector<float>& Q, // [seq_len, d_model] const vector<float>& K, const vector<float>& V, vector<float>& output, int seq_len, int d_model) { float scale = 1.0f / sqrt(d_model); // QK^T vector<float> scores(seq_len * seq_len); for (int i = 0; i < seq_len; i++) { for (int j = 0; j < seq_len; j++) { float sum = 0.0f; for (int k = 0; k < d_model; k++) { sum += Q[i * d_model + k] * K[j * d_model + k]; } scores[i * seq_len + j] = sum * scale; } } // Softmax for (int i = 0; i < seq_len; i++) { float max_score = -1e9; for (int j = 0; j < seq_len; j++) { max_score = max(max_score, scores[i * seq_len + j]); } float sum_exp = 0.0f; for (int j = 0; j < seq_len; j++) { scores[i * seq_len + j] = exp(scores[i * seq_len + j] - max_score); sum_exp += scores[i * seq_len + j]; } for (int j = 0; j < seq_len; j++) { scores[i * seq_len + j] /= sum_exp; } } // 乘以V for (int i = 0; i < seq_len; i++) { for (int k = 0; k < d_model; k++) { float sum = 0.0f; for (int j = 0; j < seq_len; j++) { sum += scores[i * seq_len + j] * V[j * d_model + k]; } output[i * d_model + k] = sum; } } }
这段代码演示了attention的基本流程。实际项目里还会做很多优化,比如KV Cache(缓存之前的K和V,避免重复计算)、多头注意力的并行等等。
整个推理过程大概是这样的:
// 简化的推理流程 class SimpleLLM { private: // 模型参数(简化) vector<vector<float>> embedding; // token嵌入 vector<vector<float>> W_q, W_k, W_v; // attention权重 vector<vector<float>> W_out; // 输出层权重 public: vector<int> generate(const vector<int>& input_tokens, int max_new_tokens) { vector<int> output = input_tokens; for (int step = 0; step < max_new_tokens; step++) { // 1. Token embedding vector<float> hidden_states = embed_tokens(output); // 2. Transformer层(简化为单层) hidden_states = transformer_layer(hidden_states); // 3. 输出层,得到下一个token的概率分布 vector<float> logits = output_layer(hidden_states); // 4. 采样(简单取最大值) int next_token = argmax(logits); output.push_back(next_token); // 5. 如果遇到结束符,停止生成 if (next_token == EOS_TOKEN) break; } return output; } private: vector<float> embed_tokens(const vector<int>& tokens) { // 简化实现 return vector<float>(); } vector<float> transformer_layer(const vector<float>& input) { // Self-attention + FFN return input; } vector<float> output_layer(const vector<float>& hidden) { // 线性层 + softmax return vector<float>(); } int argmax(const vector<float>& vec) { return max_element(vec.begin(), vec.end()) - vec.begin(); } const int EOS_TOKEN = 2; // 结束符ID };
这只是个框架,真实的LLM有几十层Transformer,还有RoPE位置编码、RMSNorm归一化、SwiGLU激活函数等等。但核心思路是一样的:不断用前面的tokens预测下一个token,直到生成结束。
看懂原理后,我尝试写了个超简化版的文本生成demo。不用真实模型,就模拟一下推理流程:
#include <iostream> #include <vector> #include <string> #include <map> #include <random> #include <cmath> using namespace std; class TinyLLM { private: map<string, int> vocab; // 词表 vector<string> id2token; int vocab_size; int embed_dim; // 简化的"模型参数"(随机初始化) vector<vector<float>> embedding; vector<vector<float>> output_weight; random_device rd; mt19937 gen; public: TinyLLM(int embed_dim = 64) : embed_dim(embed_dim), gen(rd()) { // 构建一个小词表 vector<string> words = { "<pad>", "<eos>", "hello", "world", "I", "am", "a", "student", "studying", "C++", "and", "AI" }; vocab_size = words.size(); for (int i = 0; i < vocab_size; i++) { vocab[words[i]] = i; id2token.push_back(words[i]); } // 随机初始化embedding和输出权重 embedding.resize(vocab_size, vector<float>(embed_dim)); output_weight.resize(embed_dim, vector<float>(vocab_size)); uniform_real_distribution<> dis(-0.1, 0.1); for (int i = 0; i < vocab_size; i++) for (int j = 0; j < embed_dim; j++) embedding[i][j] = dis(gen); for (int i = 0; i < embed_dim; i++) for (int j = 0; j < vocab_size; j++) output_weight[i][j] = dis(gen); } // Token转ID vector<int> tokenize(const vector<string>& tokens) { vector<int> ids; for (const auto& t : tokens) { if (vocab.count(t)) { ids.push_back(vocab[t]); } } return ids; } // 简单的前向传播 vector<float> forward(const vector<int>& input_ids) { // 1. 取最后一个token的embedding int last_token = input_ids.back(); vector<float> hidden = embedding[last_token]; // 2. 这里省略Transformer层,直接到输出层 // 计算logits = hidden * output_weight vector<float> logits(vocab_size, 0.0f); for (int i = 0; i < vocab_size; i++) { for (int j = 0; j < embed_dim; j++) { logits[i] += hidden[j] * output_weight[j][i]; } } // 3. Softmax float max_logit = *max_element(logits.begin(), logits.end()); float sum = 0.0f; for (int i = 0; i < vocab_size; i++) { logits[i] = exp(logits[i] - max_logit); sum += logits[i]; } for (int i = 0; i < vocab_size; i++) { logits[i] /= sum; } return logits; } // 生成文本 string generate(const vector<string>& prompt, int max_len = 10) { vector<int> tokens = tokenize(prompt); string result; for (const auto& t : prompt) { result += t + " "; } for (int i = 0; i < max_len; i++) { vector<float> probs = forward(tokens); // 采样(这里用top-1,即贪心) int next_token = max_element(probs.begin(), probs.end()) - probs.begin(); if (next_token == vocab["<eos>"]) break; tokens.push_back(next_token); result += id2token[next_token] + " "; } return result; } void show_vocab() { cout << "词表内容: "; for (const auto& t : id2token) { cout << t << " "; } cout << endl << endl; } }; int main() { TinyLLM model(64); model.show_vocab(); vector<string> prompt = {"I", "am"}; cout << "输入提示: "; for (const auto& t : prompt) cout << t << " "; cout << endl; string output = model.generate(prompt, 5); cout << "真实LLM需要在大规模语料上训练才能生成有意义的文本" << endl; return 0; }
编译运行:
g++ -O2 -std=c++11 -o tiny_llm tiny_llm.cpp ./tiny_llm
输出结果如下:
词表大小: 12 词表内容: <pad> <eos> hello world I am a student studying C++ and AI 输入提示: I am 生成结果: I am a student studying C++ and 注意:这是随机初始化的模型,输出没有实际意义 真实LLM需要在大规模语料上训练才能生成有意义的文本
这个demo很粗糙,但展示了推理的基本流程:embedding → 计算 → 输出概率 → 采样。真实模型复杂得多,但底层逻辑是相通的。
这几个月折腾下来,踩了不少坑,也有一些收获。
用C++写LLM相关的代码,内存管理是个大问题。一不小心就内存泄漏或者访问越界。我现在养成习惯,尽量用智能指针和STL容器,少用裸指针。但有些底层优化还是得手动管理内存,这时候valgrind就派上用场了。
实现softmax的时候,我直接算exp,结果数值溢出了。后来学到了数值稳定的技巧:先减去最大值再算exp。这种细节在做数值计算时特别重要。
开始我以为加个多线程就能快很多,结果发现线程同步的开销反而让程序变慢了。后来才明白,性能优化要先profile找瓶颈,不能盲目优化。而且很多优化技巧是相互冲突的,需要权衡。
以前觉得C++又老又难学,现在发现它在系统底层和高性能计算领域是真香。Python适合快速原型开发,但要做生产级的推理服务,C++几乎是唯一选择。
推荐几个我常看的:
说实话,AI这个领域变化太快了,一个月不看论文就感觉跟不上了。但底层的东西,比如矩阵运算、内存优化、并行计算这些,是不会过时的。扎实的C++功底,会是一个长期的竞争优势。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。