从 llama.cpp 到可调度 AI Worker:LLM Runtime 容器化部署踩坑实录

一次把本地 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 在各种环境下稳定、可移植、可运维。

这条路我替你们趟了一遍,坑也踩了,方案也稳了。

拿去用吧。🚀

相关推荐
To_OC3 小时前
跑了3个Docker容器后,我终于搞懂镜像和容器到底啥关系
后端·docker·容器
东风破_4 小时前
Docker 基础:为什么需要容器?
前端·后端·nginx
东风破_4 小时前
Docker 实战:使用 Nginx 作为容器入口代理 Node 服务
前端·后端·nginx
Lyyaoo.4 小时前
Spring Boot Validation 声明式参数校验
spring boot·后端·mysql
剩下了什么5 小时前
float32 与 float64 精度陷阱:如何在 Go 中避免错误使用
开发语言·后端·golang
Rain的Java大神之路5 小时前
介绍一下分布式事务
java·分布式·后端·spring·spring cloud·架构·springcloud
IT_陈寒9 小时前
Vue的响应式什么时候会失灵?这个坑我踩了
前端·人工智能·后端
Patrick_Wilson9 小时前
Web 认证方案技术指南
前端·后端·面试
与驴OO9 小时前
设计 Skill 系统,这 3 个坑我替你踩过了
后端·aigc
爱勇宝10 小时前
客户只想看个页面,我却做了一个静态演示发布系统
前端·javascript·后端