## 在海光 DCU 八卡机上把[多模态识图集群](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=%E5%A4%9A%E6%A8%A1%E6%80%81%E8%AF%86%E5%9B%BE%E9%9B%86%E7%BE%A4&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLlpJrmqKHmgIHor4blm77pm4bnvqQiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.ER1pguglY84vP5XYvHJrGV7BLXtgPs67gHMi3JNH-Ms&zhida_source=entity)跑起来只是上半场,下半场是**把它交给业务方接入**——而这半场的胜负,早在第一次联调之前就由「[接口契约](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=%E6%8E%A5%E5%8F%A3%E5%A5%91%E7%BA%A6&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLmjqXlj6PlpZHnuqYiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.nzcxF7tMpYbO9SLXb4lYOTHvSjoeL4kOzCvRTwB2FK8&zhida_source=entity)设计得好不好」决定了。我们的服务是 8 个 [vLLM](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=vLLM&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiJ2TExNIiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjgyMjQ2MDQ2LCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.IE-TZHGOpZO5EW5GHn_AqZBqUNO37WAG56rp0-Rmk00&zhida_source=entity) 实例(各占一张卡)前置[反向代理](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLlj43lkJHku6PnkIYiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.lAwRzHR87vlITBOZ5cnqmY946mk1iRhjfpXABk2iyFQ&zhida_source=entity)统一入口,协议上做 [OpenAI Chat Completions](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=OpenAI+Chat+Completions&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiJPcGVuQUkgQ2hhdCBDb21wbGV0aW9ucyIsInpoaWRhX3NvdXJjZSI6ImVudGl0eSIsImNvbnRlbnRfaWQiOjI4MjI0NjA0NiwiY29udGVudF90eXBlIjoiQXJ0aWNsZSIsIm1hdGNoX29yZGVyIjoxLCJ6ZF90b2tlbiI6bnVsbH0.CNkZPDUavzH7RIlx2SQ2NIp4qRbGMtGS0vPnePAPQSM&zhida_source=entity) 兼容。理论上业务方「随便找个 OpenAI SDK 就能接」,实际上接入期踩到的坑一个接一个:`user` 字段类型、think [推理链](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=%E6%8E%A8%E7%90%86%E9%93%BE&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLmjqjnkIbpk74iLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.KGPEhxIvCk6EBPlIf7hw-EQHmFWFiSmdv9M6J4wyOng&zhida_source=entity)的处理、超时设置、错误归因……
这篇是我们沉淀下来的完整接入指南——不是理想化的 API 文档,而是**每一项契约背后都标注了「不遵守会发生什么」的实战版**。任何团队在海光 DCU(或任何 GPU)上对外提供 LLM 服务,这份契约清单和排错路径都可以直接参考。
## 服务形态:接入方需要知道的最小集合
| 项 | 值 |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 模型 | [MiniCPM-V-4.5](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=MiniCPM-V-4.5&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiJNaW5pQ1BNLVYtNC41IiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjgyMjQ2MDQ2LCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.jYPdfnMfMfiPpVWR9mDu6U1r2hKSdDS-k6IiBhzt4Ps&zhida_source=entity)(8B 多模态:[Qwen3-8B](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=Qwen3-8B&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiJRd2VuMy04QiIsInpoaWRhX3NvdXJjZSI6ImVudGl0eSIsImNvbnRlbnRfaWQiOjI4MjI0NjA0NiwiY29udGVudF90eXBlIjoiQXJ0aWNsZSIsIm1hdGNoX29yZGVyIjoxLCJ6ZF90b2tlbiI6bnVsbH0.kg9wW9VWQnPYOSR3zpV3vWO-_12v5Wn6eARfdrK20pw&zhida_source=entity) 语言端 + SigLIP2-980 视觉端) |
| 入口 | http://<内网IP>:8000(反向代理,8 后端 8002–8009) |
| 协议 | OpenAI Chat Completions 兼容,HTTP 无鉴权(内网) |
| 总并发 | 32(8 卡 × 每实例 4 序列) |
| 上下文 | 32768 tokens |
| 客户端超时建议 | ≥120s(think 长输出 + 排队,单请求可到 30s+) |
「超时 ≥120s」这一条经常被忽略:很多 HTTP 客户端默认 30s 超时,而带推理链的详细描述正常就要跑十几秒,接入方会以为服务超时了,实际是自己掐断了连接。
## 请求契约:五个容易踩坑的字段
标准请求体:
```text
{
"model": "MiniCPM-V-4.5",
"messages": [{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ..."}},
{"type": "text", "text": "请详细描述这张图片的内容。"}
]}],
"max_tokens": 1024,
"temperature": 0.3,
"stream": false,
"user": "ORD-12345",
"chat_template_kwargs": {"enable_thinking": false}
}
```
逐字段标注契约和踩坑后果:
**①** **`model`** **必须精确匹配** **`"MiniCPM-V-4.5"`**(含大小写、连字符)。写错模型名是最快的 400。
**② 图片用 data URI 内嵌**。content 数组里 `image_url` 的 url 字段推荐 `data:image/jpeg;base64,...` 形式——自包含、无外部依赖。http(s) URL 形式协议上也接受,但**我们的 DCU 服务器是离线内网,拉不到外网图片**,传 URL 等于必败。MIME 按实际图片类型写。单请求最多 9 张图(`limit-mm-per-prompt` 配置)。
**③** **`user`** **必须是字符串**。这是接入期最大的一个坑:业务方把数字 ID 当整数传,vLLM 的 [Pydantic](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=Pydantic&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiJQeWRhbnRpYyIsInpoaWRhX3NvdXJjZSI6ImVudGl0eSIsImNvbnRlbnRfaWQiOjI4MjI0NjA0NiwiY29udGVudF90eXBlIjoiQXJ0aWNsZSIsIm1hdGNoX29yZGVyIjoxLCJ6ZF90b2tlbiI6bnVsbH0.PKYMXt42aG6-nKH_FYgTirRYSK_9dB4WCO7evbA31tE&zhida_source=entity) 校验直接 400——而且如果调用方吞错,会变成「服务方自测全绿、业务方全军覆没」的双盲事故。正确姿势:`"user": str(orid)`。这个字段同时是**追踪锚点**:反向代理把它记进结果日志,任何「我的某张图怎么了」的问题都能拿它 grep。
**④** **`enable_thinking`** **放在** **`chat_template_kwargs`** **里,放顶层无效**。模型默认带 `` 推理链(Qwen3 底座特性),关它的唯一正确姿势:
```text
"chat_template_kwargs": {"enable_thinking": false}
```
放顶层不会报错——这是最阴险的地方:静默不生效,你以为关了,实际每个请求还在付 think 的 token 成本。
**⑤** **`max_tokens`** **建议 ≥512**。think 平均吃 300–400 token,加上正文,384 的预算很容易顶到 `finish_reason=length` 被截断。要留余量给 1024;如果关了 think,384 足够。
`temperature` 建议显式传 0.2–0.4——模型默认 0.6(偏发散),识图类任务压低更稳。
## think 推理链:三方案权衡
默认响应的 `content` 长这样:
```text
图中有一位男性,坐在桌旁,需要判断…\n\n图片显示一位年长的男性坐在白色的桌子旁…
```
三种处理姿势,按场景选:
| 方案 | 做法 | 适用 |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| 服务端关 | 请求加 chat\_template\_kwargs.enable\_thinking=false | 批量打标、要吞吐(token 少 3–4 倍) |
| 客户端剥 | 拿到 content 后[正则](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=%E6%AD%A3%E5%88%99&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLmraPliJkiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.FgfS26nD7wfzlmKx0F5aBEXk2PzArsGtuG-QmK8bSTI&zhida_source=entity)剥 ... | 要保留思考过程的选项 |
| 原样保留 | 不处理 | 复杂图表分析,思考过程本身有价值 |
`finish_reason` 的语义也随之重要:`stop` 是正常收尾;`length` 是顶到 `max_tokens` 被截——**调用方必须检查这个字段**,被截断的描述直接入库就是[脏数据](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=%E8%84%8F%E6%95%B0%E6%8D%AE&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLohI_mlbDmja4iLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.MGR43SbosCTCBwdlSq8moLsU2hgt1_jq_bfKxiWpd1Q&zhida_source=entity)。看到大量 `length`,要么调大 `max_tokens`,要么关 think。
## 流式:[SSE](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=SSE&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiJTU0UiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.WU0XB0qhKOHNPTu8QC12B6Ew-etwtGoEHekjubfLwas&zhida_source=entity) 三行约定
`stream: true` 时返回 `text/event-stream`,约定极简:
```text
data: {"choices":[{"delta":{"content":""}}]}
data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{...}}
data: [DONE]
```
逐行读 `data:` 前缀的行、解析 JSON 取 `choices[0].delta.content`、遇到 `[DONE]` 收工。注意 usage 只在最后一帧带,要统计 token 就得攒着。
## 错误码:每一条都对应集群里的具体环节
这张表的价值在于:**错误码不是[抽象协议](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=%E6%8A%BD%E8%B1%A1%E5%8D%8F%E8%AE%AE&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLmir3osaHljY_orq4iLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.2FltKifBN4b2z3_wDgmzLR7Osgab41BGABV_q-XL_O4&zhida_source=entity),而是[架构图](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=%E6%9E%B6%E6%9E%84%E5%9B%BE&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLmnrbmnoTlm74iLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.HH81TOyqVIn63PBVzkHpWFfBZIWPhIUdGo9xQumdR0Y&zhida_source=entity)的投影**——
| HTTP | 含义 | 真实原因 | 调用方动作 |
| ---- | ------------ | ---------------------------------------------- | ------------------------------ |
| 400 | 请求格式错 | user 整数 / 模型名错 / 缺 messages / base64 坏 | 看返回体 error.message,别重试 |
| 503 | 无健康后端 | 集群重启中 / 全崩 | 稍后重试,查 /\_\_lb\_status |
| 504 | 推理超时 | 单请求超 300s(代理超时阈值) | 调小 max\_tokens / 关 think |
| 502 | 后端连接异常 | 某实例瞬时崩溃 | 可安全重试(代理已摘除它) |
关键设计:502 之后**重试是安全的**,因为代理在连接失败的那一刻已把该后端摘除,重试必然落到别的卡。400 重试是无意义的——确定性错误重试一万次还是 400。把「可重试」和「不可重试」的错误码在契约里明确分开,能省掉调用方大量无谓重试。
## 标准客户端:契约的最优载体
文档会被跳读,代码不会。我们把全部契约编译进了一个标准客户端(一两百行 Python,requests 即可运行),所有调用方强制复用:
```text
from client_45 import recognize, recognize_stream, check_service, strip_thinking
# 探活(比 /v1/models 更真实:返回几个后端健康)
ok, hp, ids = check_service() # True, "8/8"
# 非流式识图
r = recognize("test.jpg", prompt="图里有几个人?", orid="ORD-123", max_tokens=1024)
print(r["answer"]) # 已剥 think 的最终答案
print(r["finish_reason"], r["backend"], r["completion_tokens"])
# 关 think 批量跑(快 3–4 倍)
r = recognize("test.jpg", orid="ORD-124", enable_thinking=False)
# 流式
for piece in recognize_stream("test.jpg", orid="ORD-125"):
print(piece, end="", flush=True)
```
它内部封装的每一项都对应一个真实踩过的坑:`user` 强制 `str(orid)`、图片 MIME 按文件头推断(不用猜扩展名)、think 剥离、**极端长宽比图自动 padding**(超 3:1 的横条图补白边,防退化)、错误结构化解析。接入方用它的第一天起就是对的——**接口契约的正确位置不是文档,是唯一的标准实现**。
## 排错三件套:一分钟定位「我的请求怎么了」
接入后不可避免会遇到「这张图失败了」。标准排错路径三步:
1. **[响应头](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=1&q=%E5%93%8D%E5%BA%94%E5%A4%B4&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLlk43lupTlpLQiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwNDYsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.em5Nzgmp3zS53Wwakzu0AuAzwL5b9v8kk2DjrrX-KNc&zhida_source=entity)`X-Backend`**:告诉你命中了哪张卡(`127.0.0.1:8005` = 5 号实例)。同一张图反复错且后端固定 → 到那台实例日志里查;
2. **`GET /__lb_status`**:集群全景——`{"healthy": 8, "total": 8, "backends": [...]}`,`active` 是各卡在途请求数。请求失败先看这里:healthy 掉了就是实例死了,active 全满就是过载排队;
3. **代理结果日志按 orid grep**:每个请求一行,含状态码、finish\_reason、token 数、错误正文。拿着 orid 来问的,都能一条 grep 回答。
这三件套是我们「服务可运维」的底线配置——没有它们,「偶发失败」永远是玄学;有了它们,每个失败都是可归因的事件。
## 小结
接入期的摩擦,八成来自契约里没写清的隐性约定:`user` 的类型、think 的开关位置、超时的量级、错误码的重试语义。在[海光 DCU](https://zhida.zhihu.com/search?content_id=282246046&content_type=Article&match_order=3&q=%E6%B5%B7%E5%85%89+DCU&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NTcsInEiOiLmtbflhYkgRENVIiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjgyMjQ2MDQ2LCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjMsInpkX3Rva2VuIjpudWxsfQ.F5lciRHJqiSOdAvbpbgg6hlZJSe19TG7OavsvED3Vag&zhida_source=entity) 上做服务的经验是:**契约要按「调用方会犯的错」来写,而不是按「协议允许什么」来写**——每一个「必须」后面跟上「否则会发生什么」,再把全部约定编译进标准客户端。服务方多花一天做这个,接入期就少吵一周的架。
---
*环境信息:海光 K100\_AI × 8 / MiniCPM-V-4.5(8B 多模态)/ vLLM 0.11 DCU 定制版 + torch 2.5.1 + transformers 4.57.6 / 8 实例 + aiohttp 反代 :8000 / OpenAI 兼容协议。*