llama.cpp docker本地算力方案

结论与 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 专家 CPUllama.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_HOMEmodels 必须 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。