从课程作业到LLM推理:一个研二学生的C++实践笔记
前言
大家好,我是Dream。转眼间已经研二了,本科学的计算机,说实话当时C++学得挺痛苦的。指针、内存管理、模板这些东西,考试前临时抱佛脚勉强过了。真正让我重新拾起C++,是因为今年上半年做课题需要跑大模型,导师给的服务器GPU不够用,我就想着能不能优化一下推理速度。这一折腾,反而让我发现了C++在AI时代的价值。不夸张地说,现在主流的LLM推理框架,底层基本都是C++写的。作为一个研究生,我觉得这个方向挺值得深入的,就把自己这几个月的摸索记录下来。
一、我的C++入坑经历
大二学C++那会儿,最头疼的就是指针和内存管理。我记得有次写链表作业,程序老是莫名其妙崩溃,调试了一整晚才发现是忘了初始化指针。那时候就想,要是有Python那么简单就好了。
后来读研,课题组主要用Python做实验,我也就很少碰C++了。直到今年3月份,我在跑一个文本生成任务,用的Hugging Face的transformers库。模型不大,7B参数,但推理速度慢得离谱,生成一句话要好几秒。
导师说GPU资源紧张,让我想办法优化。我就开始到处找资料,发现了llama.cpp这个项目。这玩意儿挺神奇的,纯CPU跑大模型,速度比我用PyTorch快好几倍。仔细一看,全是C++写的。
二、偶然接触到的LLM世界
先说说我的理解。现在大家谈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++来榨干硬件性能。
三、动手实践:用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);
cout << "朴素版本耗时: " << duration.count() << " ms" << endl;
cout << "结果示例 C[0][0] = " << C[0][0] << endl;
}
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推理引擎会用到更多技术,比如:
• 向量化指令(SIMD):一次计算多个数据
• 多线程并行:充分利用多核CPU
• 量化技术:用int8代替float32,减少计算量
这些优化叠加起来,能让性能再提升5-10倍。
四、深入llama.cpp:看懂推理引擎
搞明白矩阵乘法后,llama.cpp 确实是个很适合深入学习 LLM 推理底层逻辑的项目,它把复杂的模型推理流程拆解得很清晰,从权重加载、token 处理到每一层的矩阵运算、激活函数计算,都能在代码里找到对应实现。
核心的推理逻辑在llama.cpp文件里,我挑几个关键点说说:
1. 模型加载与量化
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;
}
量化虽然会损失一点精度,但在大多数任务上影响不大,换来的是更快的速度和更小的内存占用。这对于在普通电脑上跑模型特别重要。
2. 注意力机制的实现
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,避免重复计算)、多头注意力的并行等等。
3. 推理流程
整个推理过程大概是这样的:
// 简化的推理流程
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
看懂原理后,我尝试写了个超简化版的文本生成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 << "词表大小: " << vocab_size << endl;
cout << "词表内容: ";
for (const auto& t : id2token) {
cout << t << " ";
}
cout << endl << endl;
}
};
int main() {
cout << "=== 简单LLM推理演示 ===" << endl << endl;
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 << "生成结果: " << output << endl;
cout << endl << "注意:这是随机初始化的模型,输出没有实际意义" << endl;
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 → 计算 → 输出概率 → 采样。真实模型复杂得多,但底层逻辑是相通的。
六、踩过的坑和收获
这几个月折腾下来,踩了不少坑,也有一些收获。
1.内存管理依然很头疼
用C++写LLM相关的代码,内存管理是个大问题。一不小心就内存泄漏或者访问越界。我现在养成习惯,尽量用智能指针和STL容器,少用裸指针。但有些底层优化还是得手动管理内存,这时候valgrind就派上用场了。
2.浮点数精度问题
实现softmax的时候,我直接算exp,结果数值溢出了。后来学到了数值稳定的技巧:先减去最大值再算exp。这种细节在做数值计算时特别重要。
3.性能优化是个无底洞
开始我以为加个多线程就能快很多,结果发现线程同步的开销反而让程序变慢了。后来才明白,性能优化要先profile找瓶颈,不能盲目优化。而且很多优化技巧是相互冲突的,需要权衡。
以前觉得C++又老又难学,现在发现它在系统底层和高性能计算领域是真香。Python适合快速原型开发,但要做生产级的推理服务,C++几乎是唯一选择。
推荐几个我常看的:
• llama.cpp项目:代码质量高,注释详细
• GGML库:专门做tensor运算的C库,很轻量
• Andrej Karpathy的视频:讲解深入浅出
• 3Blue1Brown的动画:可视化理解attention
展望
说实话,AI这个领域变化太快了,一个月不看论文就感觉跟不上了。但底层的东西,比如矩阵运算、内存优化、并行计算这些,是不会过时的。扎实的C++功底,会是一个长期的竞争优势。
- 点赞
- 收藏
- 关注作者
评论(0)