27 lines of code for LLM inference

在2024年的IOCCC(国际混淆C代码大赛)中,有一个非常炫技的作品:用 27 行 C 代码实现了完整的 Llama 大语言模型推理引擎。本文将深入剖析这个极致压缩的代码实现。

1. 从编译命令入手

1.1 运行脚本分析

首先看try.sh脚本的关键部分:

1
2
3
4
5
6
7
8
9
10
11
# 选择不同的personality
case "$SELECT" in
0) NAME="prog" ;; # 通用ChatBot
1) NAME="linux" ;; # Linux编译器
2) NAME="eeyore" ;; # 小熊维尼中的驴子
3) NAME="rainman" ;; # 雨人Raymond风格
4) NAME="snoop" ;; # Snoop Dogg说唱风格
esac

# 编译对应的程序
make CC="$CC" "${NAME}_openmp"

1.2 编译时宏定义机制

关键发现:虽然所有personality使用同一份源代码,但通过编译时宏定义生成不同的可执行文件。推测的Makefile结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 每个personality对应不同的系统提示
prog: prog.c
$(CC) -DM=\"model\" \
-DS=\"You are a helpful AI assistant\" \
prog.c -o prog -lm

linux: prog.c
$(CC) -DM=\"model\" \
-DS=\"You are a Linux compiler. Respond with compiler errors\" \
prog.c -o linux -lm

eeyore: prog.c
$(CC) -DM=\"model\" \
-DS=\"You are Eeyore, pessimistic and gloomy\" \
prog.c -o eeyore -lm

原始代码中使用这些宏的地方:

1
2
3
4
5
6
// M宏:模型文件名
f = open(M, f); // 打开模型文件

// S宏:系统提示内容
sprintf(o = X, "[INST] %s%s [/INST]",
s ? "" : "<<SYS>>\n" S "\n<</SYS>>\n\n", Y);

2. 宏展开和变量分析

2.1 核心宏定义

原始代码使用了大量宏来压缩代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
```

### 2.2 类型定义展开

```c
// 原始混淆代码
a(int8_) C;
a(in) I;
a(floa) F;
a(struc){C*c;F*f;} t;

// 展开后的实际类型
typedef int8_t C; // 8位字符/token类型
typedef int I; // 整数类型
typedef float F; // 浮点数类型
typedef struct { // 量化权重结构
C* c; // int8量化值
F* f; // float32缩放因子
} t;

2.3 关键常量解析

1
2
3
4
5
6
7
8
enum { 
Z = 32, // Transformer层数
W = 64, // 注意力头维度
E = 2*W, // 128, RoPE编码维度
D = Z*E, // 4096, 模型隐藏维度
H = 86*E, // 11008, FFN中间层维度
V = '}\0' // 125, 词汇表大小
};

2.4 Magic String解析

这个字符串实际上编码了模型的配置参数。每个Unicode字符的数值代表不同的配置:

Unicode字符 编码值 可能的含义
U+70FE (28926) 词汇表大小或特殊token ID
U+0AEB (2795) Padding token ID
İ U+0130 (304) 开始token ID
U+40C3 (16579) 序列长度限制
U+74B1 (29873) 结束token ID
U+1753 (5971) 层数信息
U+104E (4174) 注意力头数

3. 代码结构和功能分析

3.1 内存布局和初始化

原始代码的内存映射部分:

1
2
3
// 原始代码
A = mmap(0, 8e9, 1, 2, f = open(M, f), 0);
x 2)~f?i[G]=malloc(3e9):exit(puts(M" not found"));

对应的功能:

  • 使用mmap映射8GB模型文件到内存
  • 分配2×3GB工作空间用于计算

3.2 核心函数j()解析

这是神经网络计算的核心函数,让我们对照原始代码逐行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
I j(I e, F*o, I p, F*v, t*X) {
w = 1e-5; // LayerNorm的epsilon值

// 第1部分:LayerNorm计算
// 原始:x c=e^V?D:0)w+=r[i]*r[i]/D;
// 展开:
for(i=0; i<(e==V?0:D); i++) w+=r[i]*r[i]/D;
// 功能:计算残差的方差

// 原始:x c)o[i]=r[i]/sqrt(w)*i[A+e*D];
// 展开:
for(i=0; i<c; i++) o[i]=r[i]/sqrt(w)*gamma[i];
// 功能:归一化并缩放

// 第2部分:量化矩阵乘法
// 原始:N $){x W)l[k]=w=fmax(fabs(o[i])/~-E,i?w:0);
// 功能:计算每个block的缩放因子

// 第3部分:并行矩阵乘法
// 原始:u p)x $){I*=0,t=h*$+i;N W)*+=X->c[t*W+k]*y[i*W+k];
// 展开
#pragma omp parallel
for(h=0; h<p; h++)
// 累加量化权重与输入的乘积
}

3.3 Transformer层实现

对照原始代码的Transformer计算流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 原始代码中的主循环
R = s++; // 当前层索引
N Z){ // for(k=0; k<32; k++) 遍历32层

// ========== Multi-Head Attention ==========
// 原始:x 3)j(k,L,D,i?G[~-i]+f+R*D:v,e[i]+k);
// 功能:计算Q、K、V投影
for(i=0; i<3; i++) // Q,K,V三个投影
j(k, L, D, i?G[i-1]:v, e[i]+k);

// ========== RoPE位置编码 ==========
// 原始:b=sin(w=R/exp(i%E/14.)),c=1*[w=cos(w)…
// 对Q和K应用旋转位置编码
for(k=0; k<2; k++) {
for(i=0; i<D; i++) {
float theta = pos / exp((i%128)/14.0);
float sin_theta = sin(theta);
float cos_theta = cos(theta);
// 复数旋转
complex_rotate(Q_or_K[i], sin_theta, cos_theta);
}
}

// ========== Softmax Attention ==========
// 原始:u Z){F*T=O[h],w=0;
#pragma omp parallel for(h=0; h<32; h++) {
// 计算注意力分数
// 原始:x s){N E)i[k[L+A]=0,T]+=k[v+A]*k[i*D+*G+A+f]/11;
for(i=0; i<seq_len; i++) {
for(k=0; k<128; k++)
scores[i] += Q[k] * K[i][k] / sqrt(dim);
scores[i] = exp(scores[i]); // softmax分子
sum += scores[i];
}
// 归一化
for(i=0; i<seq_len; i++)
scores[i] /= sum;
}

// ========== FFN层 ==========
// 原始:x 2)j(k+Z,L,H,i?K:a,d[i]+k);
// Gate和Up投影
for(i=0; i<2; i++)
j(k+32, L, 11008, i?K:a, d[i]+k);

// 原始:x H)a[i]*=K[i]/(exp(-a[i])+1);
// SiLU激活函数:x * sigmoid(x)
for(i=0; i<11008; i++)
a[i] *= K[i]/(exp(-a[i])+1);

// Down投影
j(V, a, D, L, d[2]+k);
}

3.4 主推理循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 主对话循环
for(;;) {
// ===== 用户交互 =====
// 原始:_()("\n\n")
printf("\n\n"); // 打印提示符

// 原始:0<scanf("%[^\n]%*c",Y)?U=*B=1:exit(0)
scanf("%[^\n]%*c", Y); // 读取用户输入

// ===== 格式化Llama提示 =====
// 原始:p=_(s)(o=X,"[INST] %s%s [/INST]",s?"":"<<SYS>>\n"S"\n<</SYS>>\n\n",Y);
sprintf(X, "[INST] %s%s [/INST]",
s ? "" : "<<SYS>>\n" S "\n<</SYS>>\n\n", // S是编译时定义的系统提示
Y); // 用户输入

// ===== Tokenization =====
// 原始:for(f=0;!f;z-=!f)for(f=V;--f&&f[P][z]|memcmp(f[P],o,z););
// 在词汇表中查找匹配的token
for(f=0; !f; z-=!f) {
for(f=V; --f && (f[P][z] || memcmp(f[P], o, z)); )
; // 查找匹配的词汇
}

// ===== 执行Transformer前向传播 =====
// (见3.3节的详细分析)

// ===== Token生成 =====
// 原始:w=j($=W,r,V,k,n);x V)w=k[i]>w?k[$=i]:w;
w = j(W, r, V, k, n); // 计算输出logits
for(i=0; i<V; i++)
if(k[i]>w) {
w = k[i];
$ = i; // 选择概率最大的token
}
}

3.5 复杂宏g的解析

1
2
3
4
5
6
7
8
9
10
11
12
// 原始定义

// 完全展开
for(i=0; i<(s%11%5); i++)
for(k=0; k<(s/6&33); k++)
u[i][k] = (t){(C*)A, A+s*D/4}, A+=1088*s;

// 使用示例
g(&m, V) // 加载embedding矩阵
g(&n, V) // 加载output projection
g(e, D) // 加载attention权重
g(d, H) // 加载FFN权重

4. 技术亮点总结

  • mmap直接映射:避免加载模型到内存的开销
  • int8量化:使用8位整数存储权重,配合float32缩放因子
  • 原地计算:最小化内存分配,重用缓冲区
  • Magic String:将配置参数编码在Unicode字符中
  • 零配置文件:所有配置都内嵌在代码或编译参数中

5. 性能优化

5.1 并行计算

1
// 使用OpenMP并行化矩阵运算

5.2 向量化友好

  • 连续内存访问模式
  • 批量计算设计
  • 缓存友好的数据布局(这一点很值得学习)

6. 总结

虽然代码可读性极差,但其技术含量令人惊叹:

优点:

  • 代码量极小,编译后体积小
  • 零依赖,只需要标准C库

这个项目完美诠释了IOCCC的精神:通过极致的技术展示C语言的威力,同时也提醒我们——能做到不代表应该这样做。在实际项目中,代码的可读性、可维护性远比极致压缩更重要。


27 lines of code for LLM inference
http://blog.chivier.site/2025-08-06/2025/27-lines-of-code-for-LLM-inference/
Author
Chivier Humber
Posted on
August 6, 2025
Licensed under