1. 昇腾910B/910C 多模态模型私有化部署到底难在哪
昇腾910B/910C 多模态模型私有化部署,说白了就是把 LLM、视觉理解、文生视频、文生音频、语音识别、RAG 组件这些模型,全部跑在自家机房里的昇腾 NPU 上,再用一个统一入口对外提供服务。适合谁?适合手里已经有 Atlas 800 系列服务器、又不想把数据发到公网的团队。它解决的问题很直接:数据不出内网、推理链路可控、模型版本自己说了算。
但真动手你会发现,难点根本不在“把模型跑起来”这一步。昇腾生态里,驱动、CANN、torch_npu、推理框架这四个东西版本必须严格咬合,差一个小版本就可能算子在加载阶段直接崩。我见过最典型的情况是:单模型跑通了,一上多模型就开始互相抢卡、端口冲突、Nginx 转发 SSE 流式输出被缓冲住,客户端半天收不到第一个 token。
另一个坑是多模态接口格式根本不统一。LLM 走 OpenAI 兼容格式,视频生成走 SGLang 或 vLLM-Omni 的原生格式,语音识别又是 multipart 上传,Embedding 和 Rerank 的返回结构也不一样。如果没有一层网关做路径归一,前端要记十几个不同的地址和参数,维护成本极高。
所以这套方案的核心思路是:每个模型单实例部署,推理框架直接暴露 HTTP API,Nginx 作为唯一入口按路径路由。不引入额外的模型管理平台,少一层依赖就少一个故障点。下面从资源分配、逐模型部署命令,到 Nginx 配置和端到端验证,一步步给全。
2. TaoToken 前置:统一 Key 与模型接入准备
在讲昇腾本地部署之前,先说一个实际工程里绕不开的问题:你本地跑的是私有化模型,但团队里往往还需要调用一些云端模型做对比测试、做效果基线,或者某些轻量任务直接走云端更划算。这时候如果每个模型都单独配一套 Key 和地址,代码里会散落一堆硬编码。
TaoToken 在这里的角色是统一接入层。它提供一个兼容 OpenAI 格式的入口,你可以把云端模型的调用收敛到一个 Base URL 和一把 Key 上。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。
具体操作上,你需要在控制台创建 API Key,然后拿到模型列表。控制台地址带 utm 参数:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。创建完 Key 之后,模型对话调试页面在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,可以先用它验证 Key 是否可用。
这里要强调一个工程习惯:本地昇腾服务和云端接入的配置要分开管理。本地模型走内网 Nginx 网关,云端模型走 TaoToken 的 Base URL,两套配置通过环境变量切换,不要混在一个配置文件里。这样后面做灰度或者故障切换时,改一个变量就行。
如果你后面要接 Claude Code 这类编码工具做 Agent 开发,TaoToken 也提供了对应的接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。长期做编码和 Agent 任务的,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
需要说清楚的是,TaoToken 是统一接入层,不是替代你本地推理框架的东西。昇腾上的模型还是由 vLLM Ascend、SGLang 这些框架跑,TaoToken 负责的是把云端调用和本地调用在配置层面统一起来,方便你在同一套代码里切换。
3. 可复制配置:Nginx 网关与模型服务注册模板
这一节给的是可以直接抄的配置。先看 Nginx 的完整配置,路径是 /opt/ai-gateway/nginx.conf,把里面的 Node-X-IP 替换成你各节点的实际内网 IP。
worker_processes auto; events { worker_connections 4096; } http { upstream llm_deepseek { server Node-2-IP:8000 max_fails=3 fail_timeout=30s; keepalive 32; } upstream llm_qwen { server Node-4-IP:8001 max_fails=3 fail_timeout=30s; keepalive 32; } upstream vision_internvl { server Node-4-IP:8002 max_fails=3 fail_timeout=30s; keepalive 16; } upstream video_minimax { server Node-1-IP:8500 max_fails=3 fail_timeout=30s; } upstream video_wan { server Node-3-IP:8600 max_fails=3 fail_timeout=30s; } upstream video_step { server Node-6-IP:8700 max_fails=3 fail_timeout=30s; } upstream audio_ming { server Node-5-IP:8800 max_fails=3 fail_timeout=30s; } upstream audio_step { server Node-6-IP:8900 max_fails=3 fail_timeout=30s; } upstream audio_whisper { server Node-5-IP:9000 max_fails=3 fail_timeout=30s; } upstream embedding_bge { server Node-5-IP:9100 max_fails=3 fail_timeout=30s; keepalive 32; } upstream rerank_bge { server Node-5-IP:9200 max_fails=3 fail_timeout=30s; keepalive 16; } server { listen 80; server_name ai-gateway.yourdomain.com; client_max_body_size 200m; location /v1/chat/deepseek/ { rewrite ^/v1/chat/deepseek/(.*)$ /v1/$1 break; proxy_pass http://llm_deepseek; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 600s; proxy_buffering off; } location /v1/chat/qwen/ { rewrite ^/v1/chat/qwen/(.*)$ /v1/$1 break; proxy_pass http://llm_qwen; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 600s; proxy_buffering off; } location /v1/vision/internvl/ { rewrite ^/v1/vision/internvl/(.*)$ /v1/$1 break; proxy_pass http://vision_internvl; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 600s; proxy_buffering off; } location /v1/video/minimax/ { rewrite ^/v1/video/minimax/(.*)$ /v1/$1 break; proxy_pass http://video_minimax; proxy_set_header Host $host; proxy_read_timeout 900s; proxy_buffering off; } location /v1/video/wan/ { rewrite ^/v1/video/wan/(.*)$ /v1/$1 break; proxy_pass http://video_wan; proxy_set_header Host $host; proxy_read_timeout 900s; proxy_buffering off; } location /v1/video/step/ { rewrite ^/v1/video/step/(.*)$ /v1/$1 break; proxy_pass http://video_step; proxy_set_header Host $host; proxy_read_timeout 900s; proxy_buffering off; } location /v1/audio/ming/ { rewrite ^/v1/audio/ming/(.*)$ /v1/$1 break; proxy_pass http://audio_ming; proxy_set_header Host $host; proxy_read_timeout 600s; proxy_buffering off; } location /v1/audio/step/ { rewrite ^/v1/audio/step/(.*)$ /v1/$1 break; proxy_pass http://audio_step; proxy_set_header Host $host; proxy_read_timeout 600s; proxy_buffering off; } location /v1/audio/whisper/ { rewrite ^/v1/audio/whisper/(.*)$ /v1/$1 break; proxy_pass http://audio_whisper; proxy_set_header Host $host; proxy_read_timeout 600s; client_max_body_size 100m; } location /v1/embedding/bge/ { rewrite ^/v1/embedding/bge/(.*)$ /v1/$1 break; proxy_pass http://embedding_bge; proxy_set_header Host $host; proxy_read_timeout 120s; } location /v1/rerank/bge/ { rewrite ^/v1/rerank/bge/(.*)$ /$1 break; proxy_pass http://rerank_bge; proxy_set_header Host $host; proxy_read_timeout 120s; } location /health { access_log off; return 200 "ok\n"; add_header Content-Type text/plain; } } }路径设计规则是 /v1/<类型>/<模型名>/ 前缀区分不同模型,Nginx 通过 rewrite 去掉前缀后转发到对应后端。LLM 流式输出必须设 proxy_buffering off,否则客户端无法实时收到 token,这个坑我踩过,现象是 curl 一直卡着不返回,直到生成结束才一次性吐出来。
模型服务注册模板方面,每个模型用 Docker 启动,关键是 --device 参数要挂载正确的 davinci 设备。以 DeepSeek-V4 Flash 为例,8 卡 TP 的启动命令:
docker run -d \ --name deepseek-v4-flash \ --restart unless-stopped \ -p 8000:8000 \ --device=/dev/davinci0 --device=/dev/davinci1 --device=/dev/davinci2 --device=/dev/davinci3 \ --device=/dev/davinci4 --device=/dev/davinci5 --device=/dev/davinci6 --device=/dev/davinci7 \ --device=/dev/davinci_manager --device=/dev/devmm_svm --device=/dev/hisi_hdc \ -v /data/models/llm/DeepSeek-V4-Flash-w8a8-mtp:/model \ -v /usr/local/Ascend/driver:/usr/local/Ascend/driver \ -v /usr/local/Ascend/add-ons/:/usr/local/Ascend/add-ons/ \ -e ASCEND_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \ -e VLLM_USE_V1=1 \ vllm/vllm-ascend:v0.13.0rc3 \ --model /model \ --served-model-name deepseek-v4-flash \ --host 0.0.0.0 --port 8000 \ --tensor-parallel-size 8 \ --max-model-len 131072 \ --gpu-memory-utilization 0.85 \ --trust-remote-code \ --dtype float16 \ --enable-mtp \ --mtp-num-draft-layers 3Qwen3.8-27B 用 2 卡,端口 8001:
docker run -d \ --name qwen38-27b \ --restart unless-stopped \ -p 8001:8000 \ --device=/dev/davinci0 --device=/dev/davinci1 \ --device=/dev/davinci_manager --device=/dev/devmm_svm --device=/dev/hisi_hdc \ -v /data/models/llm/Qwen3.8-27B-Instruct:/model \ -v /usr/local/Ascend/driver:/usr/local/Ascend/driver \ -v /usr/local/Ascend/add-ons/:/usr/local/Ascend/add-ons/ \ -e ASCEND_VISIBLE_DEVICES=0,1 \ vllm/vllm-ascend:v0.13.0rc3 \ --model /model \ --served-model-name qwen3.8-27b \ --host 0.0.0.0 --port 8000 \ --tensor-parallel-size 2 \ --max-model-len 32768 \ --gpu-memory-utilization 0.9 \ --trust-remote-code \ --quantization awq \ --dtype float16InternVL2.5-78B 用 6 卡,端口 8002,注意 --limit-mm-per-prompt image=4 限制单次图片数量:
docker run -d \ --name internvl2-78b \ --restart unless-stopped \ -p 8002:8000 \ --device=/dev/davinci2 --device=/dev/davinci3 --device=/dev/davinci4 \ --device=/dev/davinci5 --device=/dev/davinci6 --device=/dev/davinci7 \ --device=/dev/davinci_manager --device=/dev/devmm_svm --device=/dev/hisi_hdc \ -v /data/models/vlm/InternVL2_5-78B:/model \ -v /usr/local/Ascend/driver:/usr/local/Ascend/driver \ -v /usr/local/Ascend/add-ons/:/usr/local/Ascend/add-ons/ \ -e ASCEND_VISIBLE_DEVICES=2,3,4,5,6,7 \ vllm/vllm-ascend:v0.13.0rc3 \ --model /model \ --served-model-name internvl2.5-78b \ --host 0.0.0.0 --port 8000 \ --tensor-parallel-size 6 \ --max-model-len 8192 \ --gpu-memory-utilization 0.85 \ --trust-remote-code \ --dtype bfloat16 \ --limit-mm-per-prompt image=4视频模型 MiniMax H3 用 SGLang,8 卡 910C,端口 8500:
docker run -d \ --name minimax-h3 \ --restart unless-stopped \ -p 8500:30000 \ --device=/dev/davinci0 --device=/dev/davinci1 --device=/dev/davinci2 --device=/dev/davinci3 \ --device=/dev/davinci4 --device=/dev/davinci5 --device=/dev/davinci6 --device=/dev/davinci7 \ --device=/dev/davinci_manager --device=/dev/devmm_svm --device=/dev/hisi_hdc \ -v /data/models/video/MiniMax-H3:/model \ -v /usr/local/Ascend/driver:/usr/local/Ascend/driver \ -v /usr/local/Ascend/add-ons/:/usr/local/Ascend/add-ons/ \ -e ASCEND_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path /model \ --served-model-name minimax-h3 \ --host 0.0.0.0 \ --port 30000 \ --tensor-parallel-size 8 \ --trust-remote-code \ --dtype bfloat16 \ --mem-fraction-static 0.85音频和 RAG 组件相对轻量。Whisper-large-v3 用 2 卡,端口 9000;BGE-M3 和 BGE-Reranker 各用 1 卡,端口 9100 和 9200,用 MIS-TEI 镜像启动。这些模型的启动命令结构一致,把 --device 和 ASCEND_VISIBLE_DEVICES 换成对应卡号即可。
健康检查脚本可以写成这样,放在 Node-6 上定时跑:
#!/bin/bash GATEWAY="http://Node-6-IP" check() { local name=$1 local url=$2 code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url") if [ "$code" = "200" ]; then echo "[OK] $name" else echo "[FAIL] $name http_code=$code" fi } check "gateway" "$GATEWAY/health" check "deepseek" "$GATEWAY/v1/chat/deepseek/models" check "qwen" "$GATEWAY/v1/chat/qwen/models" check "internvl" "$GATEWAY/v1/vision/internvl/models" check "whisper" "$GATEWAY/v1/audio/whisper/health" check "bge-m3" "$GATEWAY/v1/embedding/bge/health"4. 验证请求与成功结果:一次端到端调用
配置写完,必须做端到端验证。先确认 NPU 环境正常,每台服务器上执行:
npu-smi info source /usr/local/Ascend/ascend-toolkit/set_env.sh python3 -c "import torch; import torch_npu; print('torch_npu version:', torch_npu.__version__); print('NPU available:', torch.npu.is_available())"预期输出是 torch_npu 版本号加上 NPU available: True。如果这里是 False,后面所有模型都跑不起来,先解决驱动和 CANN 版本匹配问题。
然后验证网关健康检查:
curl http://Node-6-IP/health返回 ok 说明 Nginx 起来了。接着验证 LLM 接口,走网关的 DeepSeek 路径:
curl http://Node-6-IP/v1/chat/deepseek/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"你好"}],"max_tokens":50}'成功标志是返回 JSON 里包含 choices[0].message.content 字段,且内容非空。如果开了 stream,日志里应该能看到 Application startup complete。
视觉理解接口验证,注意 content 是数组格式:
curl http://Node-6-IP/v1/vision/internvl/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"internvl2.5-78b","messages":[{"role":"user","content":[{"type":"text","text":"请描述这张图片的内容"},{"type":"image_url","image_url":{"url":"https://example.com/test.jpg"}}]}],"max_tokens":500}'视频生成接口,返回的是视频文件:
curl http://Node-6-IP/v1/video/minimax/videos/generations \ -H "Content-Type: application/json" \ -d '{"model":"minimax-h3","prompt":"一只猫在阳光下打盹,镜头缓慢推进","num_frames":81,"width":1280,"height":720,"num_inference_steps":20}' \ --output output.mp4720P/5秒/30步的生成耗时参考是 2 到 4 分钟,2K/15秒大概 8 到 15 分钟。这个时间跟卡数和量化方式强相关,第一次跑建议先用小分辨率验证链路通不通。
语音识别接口是 multipart 上传:
curl http://Node-6-IP/v1/audio/whisper/audio/transcriptions \ -F "file=@test.wav" \ -F "model=whisper-large-v3" \ -F "language=zh"Embedding 接口验证:
curl http://Node-6-IP/v1/embedding/bge/v1/embeddings \ -H "Content-Type: application/json" \ -d '{"model":"bge-m3","input":["你好世界","Hello World"]}'预期返回 1024 维向量,响应时间在 100ms 以内。Rerank 接口:
curl http://Node-6-IP/v1/rerank/bge/rerank \ -H "Content-Type: application/json" \ -d '{"model":"bge-reranker-v2-m3","query":"什么是昇腾910?","documents":["昇腾910是华为自研的AI训练芯片","今天天气很好","昇腾910B支持64GB HBM2e显存"]}'返回相关性打分,第一条和第三条分数应该明显高于第二条。到这里,11 个模型经统一入口正常调用的链路就验证完了。
5. 本篇常见错排查:401、local proxy failed、reading choices
部署过程中最容易卡住的几个报错,这里逐个对照。
第一个是 401 Unauthorized。如果你在 Nginx 层加了鉴权,但请求没带对 header,就会返回 401。检查 Nginx 配置里是否有 auth_request 或 map 规则,确认客户端请求头里的 Authorization 格式是 Bearer 。如果是本地内网测试,可以先临时去掉鉴权验证链路,通了再加回来。
第二个是 local proxy failed。这个报错通常出现在推理框架启动阶段,原因是代理环境变量干扰了本地回环请求。检查容器内是否有 http_proxy 或 https_proxy 环境变量,有的话在启动命令里显式清掉:
-e http_proxy= -e https_proxy= -e no_proxy=localhost,127.0.0.1第三个是 reading choices 相关报错,典型信息是 KeyError: 'choices' 或 reading 'choices' failed。这多半是后端返回了错误 JSON,但客户端按成功格式解析。先用 curl 直接打后端端口,看原始返回是什么。常见原因是模型名不匹配,比如请求里写 deepseek-v4-flash 但服务注册的是 deepseek-v4,后端会返回 model not found。
第四个是 Nginx 502 Bad Gateway。按顺序排查:后端容器是否在运行(docker ps)、端口是否监听(ss -tlnp)、从网关节点能否 curl 通后端端口。如果后端容器起来了但端口不通,检查 Docker 的 -p 映射和容器内 --port 是否一致。
第五个是模型加载 OOM。昇腾上显存不足的报错信息不一定直观,可能表现为算子加载失败或进程被 kill。先降 max-model-len,再考虑换更激进的量化版本,或者增加 TP 卡数。910B 单卡 64GB,910C 单卡 96GB,分配时要留出余量。
第六个是流式输出卡顿。如果 curl 请求 LLM 接口时长时间无输出然后一次性返回,检查 Nginx 的 proxy_buffering 是否设为 off,以及 proxy_read_timeout 是否够大。视频和音频生成接口的 timeout 要设到 900s 以上。
如果你在接入云端模型做对比时遇到鉴权问题,检查 TaoToken 的 API Key 是否过期,可以在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 重新生成。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有完整的 Base URL 和参数说明。
6. 语义一致 CTA:把本地网关和统一接入串起来
整套方案跑通之后,你的架构是:昇腾节点上 11 个模型各自单实例运行,Nginx 在 Node-6 做统一入口按路径路由,客户端只需要记一个网关地址。这个结构的好处是排查直接,哪个模型出问题就查哪个容器,不影响其他服务。
日常运维建议加两个东西:一是 Prometheus 加 npu-exporter 采集 NPU 利用率和温度,配 Grafana 看板;二是给每个模型接口配健康检查,异常时告警。视频和音频生成是长耗时操作,建议接 Redis 加 Celery 做异步任务队列,避免 HTTP 连接长时间挂起。
如果你还需要在本地私有化模型之外调用云端模型做效果对比或补充能力,可以用 TaoToken 把云端调用统一起来。模型对话调试入口在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,控制台在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。长期做编码和 Agent 任务的,Coding Plan 在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API 入口统一是 https://taotoken.net/api 。
最后提醒一句:昇腾工具链更新频繁,部署前一定核对各模型官方仓库的最新适配说明,驱动、CANN、torch_npu、推理框架四个版本要对齐。我试过用旧版 CANN 跑新模型算子,加载阶段直接报 unsupported op,换到匹配版本后一次通过。版本矩阵这件事,宁可多花十分钟核对,也别等跑起来再回头查。