结论与 Pipeline 总览
llama 本地算力只有一条 pipeline,全程解耦:HF 选型 → hf 服务预下载 → 共享目录交接 → llama 服务按 INI 加载 → 宿主 NVIDIA 驱动 → frpc/frps/nginx → OpenAI 兼容公网接口。下载与 serving 是两个 compose,中间只靠 data/models 目录交接;serving 与穿透是 sidecar,跟着同一个 compose 起停。
HF 仓库(只选 GGUF)
| hf-repo / hf-file
v
docker/hf 服务 [hf-download 一次性容器, HF_HOME=/models]
| hf download, 缓存布局与 llama-server 同构, 下完即命中
v
共享目录 docker/llama/data/models (<-> /models)
| --models-preset /config/xxx.ini
v
docker/llama 服务 [llama, GPU 透传, INI 指定模型, load-on-startup 预热]
| llama_decode -> ggml-cuda -> 宿主 NVIDIA 驱动 / CUDA
v
sidecar frpc-llama -> 云 frps -> nginx(子域名, TLS 终结) -> 公网 OpenAI 兼容接口目录与服务命名
| 宿主目录 | compose 服务 | 职责 |
|---|---|---|
docker/hf/ | hf-download(一次性,restart: no) | 只管囤货:按仓库名 + 文件名预下载,不管推理 |
docker/llama/ | llama(常驻)+ frpc-llama(sidecar) | 只管 serving:按 INI 加载模型出 API;穿透跟着服务走 |
docker/llama/data/models ↔ /models | 交接面 | 两边 HF_HOME=/models 必须一致,否则缓存互不可见 |
docker/llama/data/config/*.ini ↔ /config | 模型与参数约定 | 现用与待用 INI 共存,切换 = 换 --models-preset 后 up -d |
HF 选型:只勾 GGUF
- llama.cpp 只读 GGUF 一种权重格式。HF library 过滤只选
gguf(llamafile本质也是 GGUF,可忽略)。其余(Safetensors / PyTorch / Transformers / Diffusers / ONNX / PEFT / MLX / Core ML / OpenVINO 等)一律打不开。 - 文件名必须是单个完整
.gguf(含Q4_K_M/IQ4_XS这类量化后缀的完整文件名,钉死版本)。 - 三类排除(2026-10 实测):
-00001-of-00003分片 GGUF(hf-file只下一个分片,B 方案启动缺件,改走手动全下);layers/切层包(Mesh LLM 分布式结构,llama 读不了);GPTQ / NVFP4 / Strata 专有格式(作者 README 明示不兼容 llama.cpp)。 - gated 仓库需
HF_TOKEN;新架构(如 Qwen3-Next 系)必须用近期server-cuda镜像,老版本不认架构。
hf 预下载服务
services:
hf-download:
image: python:3.12-slim
container_name: hf-download
volumes:
- /s500data/docker/llama/data/models:/models
environment:
- HF_HOME=/models
- HF_HUB_VERBOSITY=info
- HF_REPO=作者/仓库名
- HF_FILE=xxx-q4_k_m.gguf
- HF_TOKEN=${HF_TOKEN:-}
command: bash -c "pip install -q 'huggingface_hub[hf_transfer]' && hf download "$$HF_REPO" "$$HF_FILE""
restart: "no"- 用法:改
HF_REPO/HF_FILE两行 →docker compose up(前台看日志)→ 容器自行退出即下完。换模型再改再up,hf download对已存在文件自动跳过,幂等。 - 命中原理(已对源码实测):llama.cpp 的 HF 缓存(
common/hf-cache.cpp)与huggingface_hub同布局,优先级LLAMA_CACHE>HF_HUB_CACHE>HF_HOME/hub。两边HF_HOME=/models一致时预下载即命中,serving 不再重复拉取。 - 判据:
data/models/hub/models--作者--仓库名/下有完整快照再切 serving。代价:每次up附带一次pip install(约一分钟),用顺后可 bake 成自有小镜像去掉。
llama serving 服务
services:
llama:
image: ghcr.io/ggml-org/llama.cpp:server-cuda
container_name: llama
ports: ["8080:8080"]
volumes:
- ./data/models:/models
- ./data/config/my-models.ini:/config/my-models.ini:ro
environment:
- HF_HOME=/models
- CUDA_VISIBLE_DEVICES=0
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
command: --models-preset /config/my-models.ini --host 0.0.0.0 --port 8080 --api-key ${LLAMA_API_KEY:-}
restart: unless-stopped
frpc-llama:
image: snowdreamtech/frpc
container_name: frpc-llama
volumes: ["./frpc-llama.toml:/etc/frp/frpc.toml:ro"]
restart: unless-stopped- 不要用
network_mode: host旧写法;sidecar 与服务同 network,用服务名直连,少一次 LAN 暴露面。 - 鉴权只留一处:
--api-key放 llama 侧,frp 只做透传,避免两处 key 对不上背锅。api-key 经.env注入(加进.gitignore),yml 里只留${LLAMA_API_KEY}引用。 - 切换模型 = 换 INI 文件 →
docker compose up -d。本地已命中缓存时秒加载;未预下载则在重启那一刻才开始拉、大模型长时间停服——所以大模型先走 hf 服务预热。 - 两路结构定稿:A/B(显存足够全量装载 / 专家 CPU 卸载)合并为这一份
llama.yml,差异全部下沉到 INI 文件 +.env里LLAMA_PRESET一行;C(Strata NVMe 流式)因运行时独立、不吃 INI,保留单独一份llama-nvme.yml。切换运行时先down旧的再up新的(单实例纪律)。
INI 约定
version = 1
[*]
c = 8192
n-gpu-layers = 99
parallel = 1
[my-model]
hf-repo = 作者/仓库名
hf-file = xxx-q4_k_m.gguf
load-on-startup = 1
temp = 0.7- B 方案(自动下载):写
hf-repo+hf-file,首次启动联网拉取并沉淀进共享目录,之后即 A 形态(断网可起)。HF_TOKEN给 gated 仓库用。 - A 方案(本地已有):写
model = /models/xxx.gguf。分片模型必须走 A(三分片手动放齐,INI 指-00001-of-00003,llama 自动找同目录后续分片)。 - 专家 CPU 卸载(超显存 MoE):
n-cpu-moe = 99(实际 clamp 到真实 MoE 层数,99 是"全部"的惯用写法),GPU 只负责注意力等计算,专家层由 CPU 与 64GB 内存承担;可适当增大上下文。以所用镜像llama-server --help为准,参数名随版本变。 load-on-startup = 1启动即载入显存预热;为 0 或缺省则按需加载。- 参数优先级:命令行 > INI 模型专属 > INI 全局
[*]> 环境变量(LLAMA_ARG_*)> 内置默认值。--host、--port属路由器级,直接写command。
GPU 透传原理(宿主驱动,容器运行时)
Linux 宿主机不需要传统显卡直通(PCIe / VFIO,那是 VM 独占整卡的做法)。标准做法是容器 GPU 透传:宿主机装驱动 + NVIDIA Container Toolkit,容器经 GPU 设备请求获得设备与驱动库视图,镜像自带 CUDA 用户态运行时。
| 维度 | PCIe 直通 / VFIO | 容器 GPU 透传 |
|---|---|---|
| 对象 | 虚拟机 | 容器进程 |
| 归属 | 整卡从宿主机夺走独占,需 IOMMU | 宿主机照常持有,仅暴露设备节点与驱动库视图 |
| 内核 | 客户机自带内核 | 共享宿主机内核,直接调用已加载驱动 |
| llama 场景 | 不需要 | 需要此种 |
- 宿主机:只装 NVIDIA Driver + NVIDIA Container Toolkit(内核模块 +
libcuda.so经 OCI Hook 自动挂载进容器)。另装 CUDA Toolkit 属冗余。 - 容器内:必须自带用户态 CUDA 运行时(
libcudart.so、cuBLAS),缺失则回退 CPU。runtime 基镜像须为nvidia/cuda:*-runtime,不可换普通 ubuntu。 - 判据:容器内
nvidia-smi能跑只证明设备透传成功,不等于 llama CUDA 可用。 - 唯一兼容规则:容器 CUDA 版本不得高于宿主机驱动支持上限(
nvidia-smi右上角可查)。 - 调用链:
docker compose up→ nvidia-container-runtime 挂载/dev/nvidia*+ 驱动库 + 注入CUDA_VISIBLE_DEVICES→ggml_backend_load_all()加载 ggml-cuda 后端 → 初始化上下文 → 分配显存。
llama-server 内部调用链
API 客户端 --HTTP--> server_http_context (cpp-httplib: 路由/CORS/API Key/线程池)
-> server_routes (JSON 解析 / chat template / tokenization -> 构造 server_task)
-> server_queue (线程安全 FIFO, HTTP 线程 push, 推理线程消费)
-> server_context (专用单线程: 持有 llama_context, 管理 slot 池, update_slots() 主循环组 batch)
-> server_slot (每并行序列一个: IDLE/STARTED/PROCESSING_PROMPT/GENERATING/DONE_PROMPT)
-> llama_decode -> ggml_backend_sched 切图分发 -> ggml-cuda (CUDA stream / cuBLAS) -> GPU
-> common_sampler_sample() 采样 -> server_response 队列 -> HTTP 线程经 SSE/JSON 返回核心循环 update_slots():收集各 slot 待处理 token 聚成统一 llama_batch,一次 llama_decode 后各 slot 分别采样;连续批处理允许不同长度请求共享 batch。上层并发靠 slot 池 + 连续批处理,下层加速靠 ggml 插件式后端,二者以 llama_batch 与 llama_decode 为边界。llama API 不直接调 CUDA,中间隔 ggml_backend_sched(实现位于 ggml/src/ggml-cuda/)。
同一张 GPU 按模型规模有三种载法(显存占用由低到高),模式 1/2 共用 llama.yml 只换 INI,模式 3 是独立运行时:
模式1 GPU 全装载 (llama-9b.ini: n-gpu-layers=99, KV 量化保余量)
llama_decode -> ggml_backend_sched -> [全部层 -> ggml-cuda -> 显存]
显存占用 = 模型权重 + KV cache + compute buffer + CUDA context(~0.3GB)
条件: 上式之和 < 显存且留 0.5GB 余量 (8GB 卡跑 9B Q4_K_M 约 7.1GB 时 KV 8k 极易顶穿)
手段: cache-type-k/v=q8_0、降 ctx-size、或 n-gpu-layers 留 2~4 层在 CPU; 不要默认 99 一定能跑
模式2 专家 CPU 卸载 (llama-35b-a3b.ini: n-cpu-moe=99, 实际 clamp 到真实 MoE 层数)
llama_decode -> ggml_backend_sched -> 分流
|- 注意力/dense -> ggml-cuda -> 显存
|- MoE 专家权重常驻 -> ggml-cpu -> 64GB 内存 (不搬, 每 token 只激活 ~3B 在 CPU 算)
|- 跨设备传的是 hidden state (小), 不是 35B 权重 (大)
代价主因是 CPU 算力 (DDR5 带宽 + CPU/GPU 同步), 不是 PCIe 搬权重; 内存够大不换页反而有利
模式3 NVMe 流式 (llama-nvme.yml: Strata 独立运行时, 不吃 INI)
请求 -> Strata server -> 当 token 激活的专家 -> NVMe 读该专家权重 -> 显存/内存 -> 计算 -> 按策略逐出
代价: 高速 SSD 随机读 + 首 token 延迟 (冷专家 miss 读盘) + 缓存命中率依赖路由局部性
Strata 缓存/逐出策略以其 README 为准, 别套用 llama.cpp 的 mmap 心智| 模式 | 切换入口 | 显存/内存分工 | 真实瓶颈 |
|---|---|---|---|
| 1 全装载 | llama.yml + llama-9b.ini | 权重+KV+buffer 全在显存 | 显存容量,8GB 留 0.5GB 余量 |
| 2 专家 CPU | llama.yml + llama-35b-a3b.ini | 注意力在 GPU,专家常驻内存在 CPU 算 | CPU 算力 + 同步,不是 PCIe 搬权重 |
| 3 NVMe 流式 | llama-nvme.yml(Strata) | 激活专家按需从 NVMe 进显存 | SSD 随机读 + 首 token 延迟 |
公网发布:frpc sidecar + frps + nginx
- 一个穿透需求配一个 frpc sidecar,跟着它服务的 compose 走(
frpc-llama/frpc-xxx各管各)。服务up/down时穿透跟着起停,配置放服务目录里,整目录可拷走。不做中央 frpc 管所有服务(生命周期搅在一起)。 - 硬约束:frps 上的代理名全局唯一(跨全部机器与服务),用
llama8080这类带服务与端口的名字;通用名会被后上线者顶掉且不报错。 - nginx 侧 SSE 必备(否则 token 攒成整句一次返回):
proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s;,frps 侧长连接超时同步调大。TLS 在 nginx 终结,llama 只管推理。
关键约束与常见问题
| 问题 | 根因 | 解决方向 |
|---|---|---|
| 容器内 nvidia-smi 正常但报 CUDA 不可用 | runtime 镜像缺 libcudart / cuBLAS,或未以 CUDA 后端编译 | 用 server-cuda 标签或自建 CUDA runtime 阶段镜像 |
| ggml_backend_load_all 未加载 CUDA 后端 | 编译未开 -DGGML_CUDA=ON 或架构不匹配 | 确认 CMake 参数与 CUDA_DOCKER_ARCH |
| GPTQ / NVFP4 / 切层包下好了跑不了 | 非 GGUF 专有格式,llama 打不开 | 只选单文件 GGUF(见选型节) |
| 分片 GGUF 启动缺件 | hf-file 只下一个分片 | 改走 A 方案手动放齐三分片 |
| B 方案首次启动下载失败 | models 卷挂了只读,或缺 HF_HOME | models 必须 rw + 两边 HF_HOME=/models 一致 |
| 显存不足 | -ngl 过高或 KV cache 超显存 | 降 -ngl,减少 parallel 或上下文长度,或换专家 CPU 卸载 INI |
| 多卡未识别 | 只透传单卡或 CUDA_VISIBLE_DEVICES 限制 | --gpus all 或显式设置可见卡 |
| 首次推理延迟高 | CUDA kernel JIT / cuBLAS 初始化 | 预热请求或编译时实例化对应架构 |
| SSE 流式变整句返回 | nginx 缓冲了 chunked 响应 | proxy_buffering off 三件套 |
来源
DeepSeek 蒸馏(镜像构建、容器启动、服务组件、单次推理调用链、INI 预加载)+ 会话实测:HF 缓存同构(llama.cpp common/hf-cache.cpp)、两仓库排除实测(NVFP4-GPTQ-Strata、Mesh 切层包)、unsloth 上游三分片确认。Pipeline 定稿:docker/hf 预下载 → docker/llama serving → frpc sidecar 公网发布。两路结构:A/B 合并一份 llama.yml(差异下沉 INI + LLAMA_PRESET),C 独立 llama-nvme.yml。