在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++) // 累加量化权重与输入的乘积 }
|
对照原始代码的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 并行计算
5.2 向量化友好
- 连续内存访问模式
- 批量计算设计
- 缓存友好的数据布局(这一点很值得学习)
6. 总结
虽然代码可读性极差,但其技术含量令人惊叹:
优点:
这个项目完美诠释了IOCCC的精神:通过极致的技术展示C语言的威力,同时也提醒我们——能做到不代表应该这样做。在实际项目中,代码的可读性、可维护性远比极致压缩更重要。