从课程作业到LLM推理:一个研二学生的C++实践笔记

举报
是Dream呀 发表于 2025/12/04 11:29:40 2025/12/04
【摘要】 从课程作业到LLM推理:一个研二学生的C++实践笔记

前言

大家好,我是Dream。转眼间已经研二了,本科学的计算机,说实话当时C++学得挺痛苦的。指针、内存管理、模板这些东西,考试前临时抱佛脚勉强过了。真正让我重新拾起C++,是因为今年上半年做课题需要跑大模型,导师给的服务器GPU不够用,我就想着能不能优化一下推理速度。这一折腾,反而让我发现了C++在AI时代的价值。不夸张地说,现在主流的LLM推理框架,底层基本都是C++写的。作为一个研究生,我觉得这个方向挺值得深入的,就把自己这几个月的摸索记录下来。

一、我的C++入坑经历

大二学C++那会儿,最头疼的就是指针和内存管理。我记得有次写链表作业,程序老是莫名其妙崩溃,调试了一整晚才发现是忘了初始化指针。那时候就想,要是有Python那么简单就好了。

后来读研,课题组主要用Python做实验,我也就很少碰C++了。直到今年3月份,我在跑一个文本生成任务,用的Hugging Facetransformers库。模型不大,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++功底,会是一个长期的竞争优势。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。