大模型部署4个案例说清楚

4 个案例讲清楚自研大模型的部署过程

很多人训完模型会卡在一个问题:

“我的 model.safetensors 训出来了,怎么让别人用上?”

答案不是“部署”两个字,而是先回答几个问题: - 跑在服务器还是本机还是手机? - 是 LLM 还是 ASR / 视觉小模型? - 用 Python 服务,还是 C++ / Kotlin / Swift 直接跑? - 用 HuggingFace 格式、GGUF、ONNX 还是 NCNN?

下面用 4 个案例把“训练格式 → 部署格式 → 推理引擎 → 服务/端侧调用”讲清楚。


4个案例覆盖了训练模型的常用场景

1
2
3
4
5
6
7
8
9
10
11
12
训练格式
safetensors / pytorch_model.bin / .pt
│
├── 服务器 LLM ───────► HuggingFace 格式 ──► vLLM / TGI ──► FastAPI
│
├── 本地 LLM ────────► GGUF ──► llama.cpp / Ollama
│
├── 端侧 LLM ───────► GGUF ──► llama.cpp ──► JNI ──► Kotlin
│ │
│ └── 也可用 ONNX ──► ONNX Runtime Mobile
│
└── 非 LLM 小模型 ───► ONNX ──► NCNN / MNN / TFLite ──► C++ / Java / Swift

主要的注意事项

  1. 训练格式不管是 safetensors 还是 pytorch_model.bin,都不要直接部署。
  2. HuggingFace 格式给服务器,GGUF 给本地/端侧 LLM,ONNX 是中间格式,NCNN 是端侧 C++ 落地格式。
  3. FastAPI / Kotlin / Swift 都不“懂模型”,它们调用推理引擎;推理引擎才读模型文件。

案例 1:服务器部署自研 7B LLM(vLLM)

1. 训练产物是什么

1
2
3
4
5
my-7b-sft/
├── config.json
├── tokenizer.json
├── tokenizer_config.json
└── model.safetensors

这就是标准 HuggingFace 格式: - config.json:模型结构 - tokenizer*:分词器 - model.safetensors:权重

说明:vLLM 不需要 ONNX,也不需要 GGUF。它直接读 HuggingFace 格式,因为 vLLM 本身就是服务器级 LLM 推理引擎,内部自己做算子优化、PagedAttention、批处理。

2. 为什么不直接用 model.generate() 上线

transformers 的 model.generate() 能跑,但: - 不支持高并发 - 没有高效 KV Cache 管理 - 没有 OpenAI 兼容接口 - 工程化能力弱

所以生产里通常用 vLLM / TGI 这种“推理服务引擎”。

3. 启动 vLLM 服务

1
2
3
4
5
6
python -m vllm.entrypoints.openai.api_server \
--model ./my-7b-sft \
--host 0.0.0.0 \
--port 8000 \
--dtype auto \
--max-model-len 4096

参数含义: - --model:HuggingFace 模型目录,不是单个文件 - --host 0.0.0.0:允许其他机器访问;localhost 只能本机 - --port 8000:HTTP 服务端口 - --dtype auto:自动选 fp16/bf16 - --max-model-len 4096:最大上下文,防止显存爆掉

启动后你会看到类似:

1
INFO: Uvicorn running on http://0.0.0.0:8000

意思是:
模型已经加载进显存,vLLM 已经把模型包装成一个 HTTP 服务了。

4. 怎么调用这个“跑起来的模型”

vLLM 提供 OpenAI 兼容接口:

1
2
3
4
5
6
7
8
9
10
11
12
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "./my-7b-sft",
"messages": [
{"role": "system", "content": "你是一个严谨的助手"},
{"role": "user", "content": "用一句话解释 Transformer"}
],
"temperature": 0.7,
"top_p": 0.9,
"max_tokens": 256
}'

请求参数含义: - model:服务里加载的模型名 - messages:多轮对话格式 - temperature:随机性,0 最稳,1 最发散 - top_p:核采样,控制候选词范围 - max_tokens:最多生成多少 token

也可以用 Python:

1
2
3
4
5
6
7
8
9
10
11
12
from openai import OpenAI

client = OpenAI(
api_key="EMPTY", # vLLM 默认不校验 key
base_url="http://localhost:8000/v1"
)

resp = client.chat.completions.create(
model="./my-7b-sft",
messages=[{"role": "user", "content": "你好"}]
)
print(resp.choices[0].message.content)

5. FastAPI 在这里干什么

FastAPI 不加载模型,而是: - 做鉴权 - 拼系统提示 - 多模型路由 - 限流 - 转发给 vLLM

1
2
3
4
5
6
7
@app.post("/internal/chat")
async def chat(req: dict):
async with httpx.AsyncClient() as c:
return await c.post(
"http://localhost:8000/v1/chat/completions",
json=req
)

结论:服务器 LLM = HuggingFace 格式 + vLLM 起服务 + FastAPI 做业务层。


案例 2:本地 / 开发机用 Ollama 跑量化 LLM

1. 为什么要转 GGUF

HuggingFace 格式适合训练和服务端加载,但不适合: - 笔记本 - 单 CPU / 小显存 - 快速本地试跑

所以需要两步: 1. 转 GGUF:从训练生态进入 llama.cpp 推理生态 2. 量化:把 16-bit 权重压成 4-bit,省显存/内存

2. 第一步:HF → FP16 GGUF

1
2
3
python convert_hf_to_gguf.py ./my-7b \
--outfile my-7b-f16.gguf \
--outtype f16

参数说明: - ./my-7b:HuggingFace 模型目录 - --outfile:输出的 GGUF 文件 - --outtype f16:权重保持 16 位浮点,不做量化
👉 这一步只是“换容器”,不是压缩

3. 第二步:FP16 GGUF → Q4_K_M GGUF

1
2
3
4
./build/bin/llama-quantize \
my-7b-f16.gguf \
my-7b.Q4_K_M.gguf \
Q4_K_M

参数说明: - 输入:FP16 GGUF - 输出:量化后 GGUF - Q4_K_M:4-bit 左右、质量/体积平衡最好的量化档位之一
- 7B 模型:FP16 约 14GB,Q4_K_M 约 4–5GB

重要:永远从 FP16 GGUF 再量化,不要从 Q4 再量化成 Q2,否则误差会叠加。

4. Modelfile 是干什么的

Modelfile 不是模型文件,也不是镜像,而是“模型 + 行为配置”的说明书,类似 Dockerfile。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM ./my-7b.Q4_K_M.gguf

SYSTEM """
你是一个中文技术助手,回答简洁、准确、不废话。
"""

TEMPLATE """
{{ if .System }}system
{{ .System }}
{{ end }}user
{{ .Prompt }}
assistant
"""

PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER num_ctx 4096
PARAMETER repeat_penalty 1.1

逐行说明:

  • FROM:底座模型,这里直接指向本地 GGUF
  • SYSTEM:每次对话都带上的“人设 / 规则”
  • TEMPLATE:把 system / user / assistant 拼成模型训练时见过的格式
    👉 模板错了,模型就会“不听指令”
  • temperature:创造性
  • top_p:采样范围
  • num_ctx:上下文窗口大小
  • repeat_penalty:防复读

5. 注册并运行

1
2
ollama create my-7b -f Modelfile
ollama run my-7b "用一句话解释向量数据库"

Ollama 默认起在:

1
http://localhost:11434

OpenAI 兼容接口:

1
POST http://localhost:11434/v1/chat/completions

FastAPI / 前端 / LangChain 都可以直接连。

结论:Ollama = 本地 LLM 运行时;GGUF = 模型文件;Modelfile = 行为配置。


案例 3:Android 端侧跑 LLM

1. 为什么端侧不能用 HuggingFace 格式直接跑

Android 上常见问题: - 没有 Python - 显存不存在,只有 RAM / NPU - 需要 APK 内嵌模型 - 要 C++ 层做高性能推理

所以路径是:

1
safetensors → GGUF → llama.cpp(C++) → JNI → Kotlin

2. 转换过程补充

1
2
3
4
5
# 1. HF → FP16 GGUF
python convert_hf_to_gguf.py ./my-1.5b --outtype f16 --outfile my-1.5b-f16.gguf

# 2. 量化
./build/bin/llama-quantize my-1.5b-f16.gguf my-1.5b-Q4_0.gguf Q4_0
  • Q4_0:端侧常用,体积小,速度块,质量略降
  • 1.5B Q4 大概 1GB 出头,适合手机

3. Android 项目结构

1
2
3
4
app/
├── jniLibs/arm64-v8a/libllama.so
├── assets/my-1.5b-Q4_0.gguf
└── java/com/example/LlamaEngine.kt

4. Kotlin 通过 JNI 调 C++

1
2
3
4
5
6
7
8
class LlamaEngine {
companion object {
init { System.loadLibrary("llama") }
}

external fun initModel(path: String): Boolean
external fun generate(prompt: String): String
}

JNI 层:

1
2
3
4
5
6
7
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_LlamaEngine_generate(JNIEnv* env, jobject, jstring prompt) {
const char* p = env->GetStringUTFChars(prompt, nullptr);
// llama.cpp 加载 GGUF、跑 decode
std::string out = llama_generate(p);
return env->NewStringUTF(out.c_str());
}

关键点: > Kotlin 不“理解” GGUF,它只是通过 JNI 调用 C++ 推理引擎;真正读 GGUF 的是 llama.cpp。

5. 如果 Android 想用 ONNX 而不是 GGUF

那就不走 llama.cpp,改走 ONNX Runtime Mobile:

1
2
3
4
5
PyTorch 模型
→ torch.onnx.export
→ model.onnx
→ 放 assets/
→ ONNX Runtime Android 加载

Kotlin:

1
2
3
4
5
val env = OrtEnvironment.getEnvironment()
val session = env.createSession(assets.open("model.onnx").readBytes())

val input = OnnxTensor.createTensor(env, inputIds, longArrayOf(1, seqLen))
val result = session.run(mapOf("input_ids" to input))

但注意: - ONNX 更适合分类 / 检测 / OCR / 小模型 - 端侧自回归 LLM 通常 GGUF + llama.cpp 更成熟


案例 4:端侧 ASR(Conformer → ONNX → NCNN)

1. Conformer 模型和前面 3 个一样吗

不一样在任务域:

案例 模型 任务
1 Decoder LLM 文本生成
2 Decoder LLM 本地对话
3 小 LLM 端侧对话
4 Conformer / Zipformer 语音识别

Conformer 也是 PyTorch 训出来的,但: - 输入是 log-Mel 声学特征 - 输出是 token / CTC / RNNT - 不是聊天模型

2. PyTorch → ONNX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch

model = MyConformer()
model.load_state_dict(torch.load("conformer.pt"))
model.eval()

dummy = torch.randn(1, 300, 80) # batch, time, mel_bins

torch.onnx.export(
model,
dummy,
"conformer.onnx",
input_names=["features"],
output_names=["logits"],
opset_version=17,
dynamic_axes={
"features": {1: "time"},
"logits": 1: "time"
}
)

目的: > 把“PyTorch 动态图”变成“静态计算图”,方便跨框架和端侧引擎加载。

3. ONNX → NCNN

1
2
3
4
5
6
7
8
# 先简化
python -m onnxsim conformer.onnx conformer-sim.onnx

# ONNX 转 NCNN
onnx2ncnn conformer-sim.onnx conformer.param conformer.bin

# NCNN 优化
ncnnoptimize conformer.param conformer.bin conformer-opt.param conformer-opt.bin 0

产物: - .param:网络结构 - .bin:权重

4. C++ 直接跑

1
2
3
4
5
6
7
8
9
10
11
12
#include "net.h"

ncnn::Net net;
net.load_param("conformer-opt.param");
net.load_model("conformer-opt.bin");

ncnn::Mat in = ncnn::Mat(w, h, features_ptr);
ncnn::Extractor ex = net.create_extractor();
ex.input("features", in);

ncnn::Mat out;
ex.extract("logits", out);

这里没有 Python、没有 Docker、没有 HTTP,就是 C++ 读文件、跑矩阵运算。


Conformer ASR


大模型部署4个案例说清楚
https://jycpp.github.io/2025/25-10-19-大模型部署4个案例说清楚.html
作者
Jet Yan
发布于
2025年10月19日
许可协议