一次把本地 LLM(llama.cpp + GGUF)跑进 K3s 的完整工程实践
一、先说说我们到底在折腾啥
坦白讲,这事儿一开始我没想那么复杂。不就是把 llama.cpp 打个镜像,扔进 K3s 跑起来吗?结果一动手才发现,坑是一个接一个。
其实这是个典型的 AI 推理分层架构:
markdown
任务调度层(K8s / K3s)
↓
Worker(任务执行层)
↓
Inference Runtime(llama.cpp)
↓
模型文件(GGUF)
我给自己定的目标很朴实:
✅ 搞一个能被 Docker / K3s 调度的 AI Worker 镜像
最终长这样:
markdown
Worker
+ llama.cpp Runtime
+ GGUF 模型
要做到:
- ✅ 一键启动,不用在宿主机上装任何东西
- ✅ 镜像就是完整的运行环境
- ✅ K3s 能随便调度
- ✅ 将来想上 CPU 或 GPU 都行
二、踩坑实录(这才是最值钱的部分)
下面这些坑,我基本上都亲自踩过,有些甚至反复踩了两三遍。
💣 坑 1:我差点把整个 llama.cpp 源码塞进镜像
❌ 我当时是这么想的
llama.cpp 是核心依赖,那不得把整个 repo 都打进去吗?
然后镜像里就变成了:
css
llama.cpp/
├── src
├── ggml
├── tests
├── examples
├── docs
...(一大堆)
问题在哪儿?
这哪是运行环境啊,这分明是把开发环境打包了。镜像又大又乱,还一堆用不上的东西。
✅ 后来想明白了
运行时真正需要的就三样:
diff
llama-server(可执行文件)
+ GGUF 模型文件
+ 必要的运行时库
打个比方:
你装 Nginx 的时候,会把 Nginx 源码一起装了吗?不会吧。
最终镜像应该是清爽的:
bash
/app
├── worker
├── llama-server
└── model.gguf
💣 坑 2:只拷 llama-server 二进制?呵呵,跑不动
❌ 我天真了
费了半天劲编译出 llama-server,拷进容器一跑:
rust
libllama-server-impl.so not found
🔍 啥情况?
默认编译 llama.cpp 的时候,llama-server 是动态链接的,它依赖一堆 so 文件:
erlang
libllama.so
libggml.so
libggml-cpu.so
...
你以为拷了一个文件,实际上它背后拖家带口。
✅ 怎么解决的
编译的时候关掉 shared libs:
bash
-DBUILD_SHARED_LIBS=OFF
重新编译后,llama-server 就成了静态内部链接版本,所有依赖都打进去了。
结果就是:
一个文件,拎包入住。
💣 坑 3:我动过"完全静态编译"的歪念头
❌ 理想很丰满
既然要打包,那干脆来个狠的:
dockerfile
FROM scratch
COPY llama-server
一个二进制,走遍天下,多酷啊!
❗ 现实给了我一巴掌
C/C++ 的生态根本做不到"无依赖":
- glibc(逃不掉)
- libstdc++(逃不掉)
- OpenMP(逃不掉)
- pthread(逃不掉)
最要命的是:
❌ glibc 静态链接在生产环境就是个定时炸弹
✅ 还是老实点吧
业界通用的做法:
统一基础镜像 + 动态 runtime
比如:
Ubuntu 24.04 编译 → Ubuntu 24.04 运行
版本锁死,别瞎折腾。
💣 坑 4:编译环境和运行环境版本不一致,直接崩
❌ 典型报错现场
GLIBC_2.38 not found
GLIBCXX_xxx not found
🔍 一查原因
我在 WSL(Ubuntu 最新版)上编译,然后往 Ubuntu 22.04 的容器里扔。
这不找死吗?
C++ 二进制对系统 ABI 是强依赖,glibc 版本不对就是跑不起来。
✅ 解决方案简单粗暴
编译环境:Ubuntu 24.04
运行环境:Ubuntu 24.04
别问为什么,锁死就完了。
💣 坑 5:Docker build 被本地编译产物污染了
❌ 现象
objectivec
CMakeCache.txt
里面还记录着我本机的路径
进容器编译直接路径错乱,一脸懵。
✅ 解决方案
写个 .dockerignore:
objectivec
build/
CMakeCache.txt
CMakeFiles/
核心原则:
❗ Docker 只吃源码,别把本地编译的垃圾带进去
💣 坑 6:删了 tests 目录,CMake 直接罢工
❌ 我干了啥
我觉得 tests 没用,直接删了。
❌ 然后呢
scss
add_subdirectory(tests) failed
CMake 在配置文件里明确写了要加 tests,结果目录没了,直接报错退出。
✅ 正确的做法
不要物理删除,而是关掉构建选项:
bash
-DLLAMA_BUILD_TESTS=OFF
-DLLAMA_BUILD_EXAMPLES=OFF
-DLLAMA_BUILD_BENCHMARKS=OFF
源码留着就留着,反正不进镜像,不影响。
💣 坑 7:CPU 指令集不兼容,换个机器就跑不了
❌ 我当时图省事
bash
-march=native
编译的时候用了本机所有的 CPU 特性,跑起来飞快。
❌ 然后呢
换了一台机器:
illegal instruction
因为新机器的 CPU 不支持某些旧指令,直接崩。
✅ 解决方案
bash
-DGGML_NATIVE=OFF
这样编译出来的二进制是兼容的,虽然牺牲了一点性能,但能到处跑。
在 K3s 集群里,你根本不知道 Pod 会调度到哪台机器上,兼容性比极致性能重要。
💣 坑 8:运行时缺 OpenMP
❌ 报错
libgomp.so.1 not found
✅ 解决方案
在运行镜像里加上:
bash
apt install libgomp1
别看这玩意儿小,没有它 llama.cpp 就废了。
三、最终架构(踩完坑后的稳定方案)
阶段 1:编译 llama.cpp
diff
基础镜像:Ubuntu 24.04
工具链:cmake + gcc + git
编译选项:
- 关闭 tests
- 关闭 examples
- 关闭 benchmarks
- 关闭 shared libs(静态链接)
输出:llama-server 单个二进制
阶段 2:编译 Worker
用 Golang 编译 Worker 调度器
输出:worker 二进制
阶段 3:组装运行镜像
diff
基础镜像:Ubuntu 24.04(和编译环境保持一致)
装依赖:
+ libgomp1
放文件:
+ worker
+ llama-server
+ GGUF 模型文件
四、最终目录结构
bash
worker:v1
/app
├── worker # 调度器
├── llama-server # 推理引擎
└── model.gguf # 模型文件
干净、清爽、没废话。
五、跑在 K3s 上咋样?
✅ 非常合适
尤其适合小规模 AI Infra 场景:
Pod
├── Worker 容器
├── llama 运行时
└── 模型文件(可以挂载,也可以打包)
K3s 轻量、简单,正好配这种"一个 Pod 干一件事"的玩法。
六、血泪经验总结(划重点)
这次折腾下来,我觉得最有价值的就这 7 条:
✅ 1. 镜像只打包运行产物,别带源码
源码是给开发用的,运行时不需要。
✅ 2. 编译环境和运行环境必须用同一个 Ubuntu 版本
C++ 的 ABI 兼容性不是闹着玩的,版本锁死最省心。
✅ 3. 别迷信完全静态编译
glibc 静态链接在生产环境容易出诡异问题,老老实实用动态链接 + 统一基础镜像。
✅ 4. C++ 程序对系统 ABI 敏感,要当成第一优先级对待
Go 写惯了会觉得"随便拷个二进制就能跑",C++ 真不行。
✅ 5. 别绑定 CPU 指令集
在 K8s/K3s 集群里,Pod 可能跑到任何节点上,兼容性优先。
✅ 6. 删除源码目录前,先确认 CMake 里有没有引用
物理删除不如关构建选项,干净又卫生。
✅ 7. 运行时依赖要补齐
别以为编译过了就万事大吉,运行时还缺 OpenMP 这种库,记得提前装上。
🚀 最后说一句
把 llama.cpp 跑进 K3s,说到底不是在"跑模型", 而是在构建一个可被调度的 AI Runtime 系统。
模型只是数据,真正的工程在于怎么让 runtime 在各种环境下稳定、可移植、可运维。
这条路我替你们趟了一遍,坑也踩了,方案也稳了。
拿去用吧。🚀