189 lines
6.2 KiB
Markdown
189 lines
6.2 KiB
Markdown
# local-embedding
|
|
|
|
本地嵌入(embedding)服务。**完整替代 Ollama** —— 零 API Key、文档不出内网,
|
|
对外提供 OpenAI 兼容的 `POST /v1/embeddings`。
|
|
|
|
## 为什么不用 Ollama
|
|
|
|
给 [PandaWiki](https://github.com/chaitin/PandaWiki) 做本地化部署时,嵌入模型
|
|
本来是走 Ollama 的,但为了这一件事装了一整套 Ollama,代价有三:
|
|
|
|
1. **Ollama 不归你管** —— 它由 fnOS 应用中心的 `ai_installer` 托管,
|
|
进程和监听地址都不在自己的控制范围内;
|
|
2. **官方 registry 不可达** —— `registry.ollama.ai` 的 blob 下载会完全卡死
|
|
(实测 30 分钟零进度),`hf-mirror.com` 也连不上,最后只能绕道 ModelScope;
|
|
3. **需要一层转发** —— Ollama 只监听 `127.0.0.1`,容器访问不到,得额外加一个
|
|
socat 容器把 docker 网关转发过去。
|
|
|
|
这个服务把这三件事都消掉了:一个容器、一个镜像、启动时自动下模型。
|
|
|
|
## 与 Ollama 的实测差异
|
|
|
|
同一批文本,本服务(ONNX int8)与 Ollama(bge-m3 GGUF Q8_0)的向量余弦相似度:
|
|
|
|
| 文本 | 相似度 |
|
|
|---|---|
|
|
| 如何部署 PandaWiki 知识库 | 0.9848 |
|
|
| PandaWiki 支持 Docker 部署,需要先配置向量模型 | 0.9821 |
|
|
| 今天上证指数涨跌如何 | 0.9849 |
|
|
|
|
内部相对关系也一致(0.7286 / 0.7390、0.3663 / 0.3664)。差异来自量化方式不同,
|
|
属于预期范围,不是实现偏差。维度同为 1024。
|
|
|
|
## 快速开始
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
首次启动会下载约 563MB 模型到 `./models`:
|
|
|
|
```
|
|
[local-embedding] 模型目录: /models
|
|
[model_int8.onnx] 0.0B / 542.1MB
|
|
[model_int8.onnx] 542.1MB / 542.1MB (100%)
|
|
[model_int8.onnx] 校验通过
|
|
[local-embedding] 模型准备完成
|
|
```
|
|
|
|
之后重启或重建容器都是零下载:
|
|
|
|
```
|
|
[model_int8.onnx] 已存在且校验通过 (542.1MB)
|
|
[local-embedding] 模型已就绪,跳过下载
|
|
```
|
|
|
|
服务监听 **63002**:
|
|
|
|
```bash
|
|
curl -s localhost:63002/health
|
|
# {"status":"ok","model":"bge-m3"}
|
|
|
|
curl -s -X POST localhost:63002/v1/embeddings \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"model":"bge-m3","input":["如何部署知识库","今天天气不错"]}'
|
|
```
|
|
|
|
## 设计要点
|
|
|
|
### 池化:CLS + L2 归一化
|
|
|
|
ONNX 导出的第一个输出可能是 `[batch, hidden]`(已池化)也可能是
|
|
`[batch, seq, hidden]`(token 级),两种都兼容;后者取 `[CLS]`(第 0 个 token)。
|
|
|
|
bge 系列官方推荐 **CLS 池化 + L2 归一化**,归一化后余弦相似度与内积等价,
|
|
调用方随便用哪种度量都对。实测输出 L2 范数为 1.000000。
|
|
|
|
### 模型不在镜像里,启动时按需下载
|
|
|
|
`download_model.py` 在 entrypoint 里先跑,做三件事:
|
|
|
|
1. **检查** —— 每个文件比对 `大小 + sha256`,全通过就立刻返回,零网络请求
|
|
2. **续传** —— 下到 `.part`,重试带 `Range` 头从断点继续
|
|
3. **原子替换** —— 校验通过才 `os.replace` 成正式文件,
|
|
避免中断留下的半个文件被当成有效模型加载
|
|
|
|
还有**磁盘预检**:空间不够直接报明确错误,而不是刷一片 `no space`。
|
|
|
|
### 为什么选 int8
|
|
|
|
带 AVX512-VNNI 的 CPU 上 int8 推理能直接吃到指令集加速。542MB 的 int8
|
|
vs 2.1GB 的 fp32,体积差 4 倍,精度损失很小 —— 见上面的对比数据。
|
|
|
|
注意 `onnx/model.onnx` 只有 0.6MB,它的权重在外置的 `model.onnx_data`(2.1GB)里;
|
|
`model_int8.onnx` 是自包含的单文件,少一个踩坑点。
|
|
|
|
## 接口
|
|
|
|
### `POST /v1/embeddings`
|
|
|
|
OpenAI 标准。请求:
|
|
|
|
```json
|
|
{
|
|
"model": "bge-m3",
|
|
"input": ["文本1", "文本2"],
|
|
"encoding_format": "float"
|
|
}
|
|
```
|
|
|
|
`input` 可以是单个字符串、字符串数组,或 token id 数组(三种可混排)。
|
|
响应:
|
|
|
|
```json
|
|
{
|
|
"object": "list",
|
|
"data": [
|
|
{"object": "embedding", "index": 0, "embedding": [0.0123, ...]}
|
|
],
|
|
"model": "bge-m3",
|
|
"usage": {"prompt_tokens": 7, "total_tokens": 7},
|
|
"_meta": {"latency_ms": 141, "dimension": 1024}
|
|
}
|
|
```
|
|
|
|
`_meta` 是本服务额外加的非标准字段,方便排查延迟和维度,不影响兼容性。
|
|
|
|
同时接受 `/embeddings`、`/v1/embed`、`/embed`。
|
|
|
|
### 其它
|
|
|
|
- `GET /health` —— 存活探测
|
|
- `GET /info` —— 当前模型、输入输出张量名、参数
|
|
- `GET /v1/models` —— 兼容部分客户端的启动探测
|
|
|
|
## 配置
|
|
|
|
全部写在 `docker-compose.yml` 的 `environment` 里,改完 `docker compose up -d` 生效。
|
|
|
|
| 变量 | 默认 | 说明 |
|
|
|---|---|---|
|
|
| 宿主机端口 | `63002` | compose 的 `ports`,映射到容器 8000 |
|
|
| `MODEL_DIR` | `/models` | 容器内模型目录 |
|
|
| `MODEL_REPO` | `Xenova/bge-m3` | 换模型改这里 |
|
|
| `MODEL_BASE` | ModelScope | 换下载源改这里 |
|
|
| `ONNX_FILE` | `model_int8.onnx` | 换量化档位 |
|
|
| `MAX_LEN` | `512` | 截断长度,bge-m3 原生支持 8192,但 CPU 上长序列很慢 |
|
|
| `BATCH_SIZE` | `16` | 每批条数 |
|
|
| `WORKERS` | `1` | 调大前先看内存,每个 worker 各加载一份模型 |
|
|
|
|
同仓库可选量化档位:`model_fp16.onnx`(1081MB)、`model_q4f16.onnx`(668MB)、
|
|
`model_q4.onnx`(1190MB)。
|
|
|
|
## 实测性能
|
|
|
|
MacBook(Docker Desktop,arm64 模拟)与 i7-1165G7:
|
|
|
|
| 场景 | 耗时 |
|
|
|---|---|
|
|
| 40 条批量 | 449 ms(约 90 条/秒) |
|
|
| 5 条批量 | 141 ms |
|
|
| 镜像大小 | 401 MB |
|
|
| 模型大小 | 563 MB |
|
|
|
|
## 已知限制
|
|
|
|
- **单 worker**:默认 `--workers 1`,每个 worker 会各加载一份模型(约 2GB 内存峰值)。
|
|
高并发应调 `WORKERS`,同时注意内存翻倍。
|
|
- **不鉴权**:不校验 `api_key`。放到非受信网络需自行加反向代理。
|
|
- **仅 CPU**:未接 ONNXRuntime-GPU。
|
|
- **只支持 ONNX**:换 safetensors 需改 `MANIFEST` 并引入 torch。
|
|
- **长文本默认截断到 512**:bge-m3 能力上限是 8192,但 CPU 上超过 512 明显变慢。
|
|
需要处理长文档时调 `MAX_LEN` 并实测延迟。
|
|
|
|
## 文件
|
|
|
|
```
|
|
local-embedding/
|
|
├── app.py FastAPI 服务(OpenAI 兼容)
|
|
├── download_model.py 模型检查/下载/校验(幂等,支持续传)
|
|
├── entrypoint.sh 先下模型,再起 uvicorn
|
|
├── requirements.txt 运行时依赖(无 torch)
|
|
├── Dockerfile
|
|
├── docker-compose.yml 参数都在这,端口 63002
|
|
├── README.md
|
|
└── .gitignore
|
|
```
|
|
|
|
配套的 [local-remarker](../local-remarker) 是同一套思路的重排服务,
|
|
接口为 SiliconFlow 格式。两个服务可以独立部署,也可以放在同一个 compose 里。 |