- 一、背景与动机
- 二、硬件环境
- 三、整体架构
- 8卡完整拓扑总览
- 单卡内部详细拓扑(以卡2为例,8卡格局完全相同)
- 8卡完整端口矩阵与路由对照表
- 推理引擎层:vllm-ascend
- OCR处理层:paddlex
- 负载均衡层:uvicorn网关
- 四、完整端口规划
- vllm-ascend(每卡2个,共16个)
- paddlex(每卡6个,共48个)
- paddlex → vllm 路由规则
- 五、vllm-ascend 推理引擎部署
- 5.1 启动脚本
- 5.2 容器创建
- 5.3 模型加载验证
- 六、paddle + paddlex OCR处理层部署
- 6.1 Pipeline配置文件
- 6.2 Batch Size优化
- 6.3 服务启动
- 七、负载均衡网关
- 核心能力
- 配置驱动
- Failover 流程
- 客户端调用示例
- 验证负载均衡
- 八、Docker Compose 编排
- 8.1 all-docker-compose.vllm.yml
- 8.2 all-docker-compose.paddle.yml
- 九、一键部署与运维
- 9.1 setup.sh 自动化部署
- 9.2 Pipeline配置文件自动生成
- 9.3 负载均衡配置自动更新
- 9.4 ops.sh 运维工具
- 十、设计决策与经验总结
- 为什么不能直接用Nginx做负载均衡?
- 为什么paddlex不随容器自动启动?
- 单卡多实例的GPU内存规划
- 命名规范的工程价值
- 结语
一、背景与动机
在大规模企业文档处理的OCR场景中,需要对海量扫描件/文档图片进行高并发识别。单实例PaddleOCR面对大量请求时吞吐量有限,因此需要在 **华为Atlas 800 TA2**(8卡Ascend NPU)服务器上部署多实例PaddleOCR,并通过自研的 **uvicorn负载均衡网关** 统一对外暴露API。
整个方案用到了三层架构:
- **vllm-ascend**(推理引擎):每张NPU卡运行2个vllm实例,负责VL模型的推理加速
- **paddlex**(OCR pipeline):每卡运行4-6个paddlex实例,调用vllm完成OCR全流程
- **uvicorn网关**(负载均衡):FastAPI实现的智能负载均衡层,提供统一的HTTP API
二、硬件环境
| 项目 | 配置 |
|---|---|
| 服务器 | Atlas 800 TA2 |
| NPU | 华为Ascend 910B × 8(卡0~卡7) |
| 操作系统 | EulerOS / openEuler |
| 容器运行时 | Docker(非K8s) |
| 镜像 | `vllm-ascend:v0.19.1rc1` / `paddleocrvl:20260529` |
| 模型 | PaddleOCR-VL-1.5-0.9B |
> 注意:服务器已被其他平台纳管,不能使用Kubernetes,因此采用纯Docker + Compose编排方案。
三、整体架构
整个系统分为四层:Uvicorn 网关 → paddlex OCR Pipeline → vllm 推理引擎 → NPU 硬件。下面分别用高层总览和单卡详细拓扑来展示。
8卡完整拓扑总览
下图展示全部8张NPU卡、16个vllm实例、48个paddlex实例的完整拓扑关系。每列的"✓"表示已有实例,"新增"表示本次扩容增加的实例。
┌─────────────────────────────────────────────────────────────────────────┐
│ Uvicorn 负载均衡网关 :9101 │
│ round_robin × 48 upstreams + failover │
└───────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┐
│ 卡0 │ 卡1 │ 卡2 │ 卡3 │ 卡4 │ 卡5 │ 卡6 │ 卡7 │
│ (全部新增) │ (全部新增) │ (已有+新增)│ (已有+新增)│ (已有+新增)│ (已有+新增)│ (已有+新增)│ (已有+新增)│
│ davinci0 │ davinci1 │ davinci2 │ davinci3 │ davinci4 │ davinci5 │ davinci6 │ davinci7 │
├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
│ │ │ │ │ │ │ │ │
│ vllm:9031 │ vllm:9032 │ vllm:9025✓│ vllm:9026✓│ vllm:9027✓│ vllm:9028✓│ vllm:9029✓│ vllm:9030✓│
│ ├─9126 │ ├─9127 │ ├─9120 │ ├─9121 │ ├─9122 │ ├─9123 │ ├─9124 │ ├─9125 │
│ ├─9136 │ ├─9137 │ ├─9130 │ ├─9131 │ ├─9132 │ ├─9133 │ ├─9134 │ ├─9135 │
│ └─9146 │ └─9147 │ └─9140 │ └─9141 │ └─9142 │ └─9143 │ └─9144 │ └─9145 │
│ │ │ │ │ │ │ │ │
│ vllm:9041 │ vllm:9042 │ vllm:9035 │ vllm:9036 │ vllm:9037 │ vllm:9038 │ vllm:9039 │ vllm:9040 │
│ ├─9156 │ ├─9157 │ ├─9150 │ ├─9151 │ ├─9152 │ ├─9153 │ ├─9154 │ ├─9155 │
│ ├─9166 │ ├─9167 │ ├─9160 │ ├─9161 │ ├─9162 │ ├─9163 │ ├─9164 │ ├─9165 │
│ └─9176 │ └─9177 │ └─9170 │ └─9171 │ └─9172 │ └─9173 │ └─9174 │ └─9175 │
├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
│ paddle×6 │ paddle×6 │ paddle×6 │ paddle×6 │ paddle×6 │ paddle×6 │ paddle×6 │ paddle×6 │
│ vllm×2 │ vllm×2 │ vllm×2 │ vllm×2 │ vllm×2 │ vllm×2 │ vllm×2 │ vllm×2 │
└────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┘
│
┌────────┴────────┐
│ Ascend 910B×8 │
│ Atlas 800 TA2 │
└─────────────────┘
✓ = 已有实例 │ 无标记 = 新增实例 │ ├─ = paddlex → vllm 路由绑定
单卡内部详细拓扑(以卡2为例,8卡格局完全相同)
每张NPU卡上的拓扑完全一致:2个vllm实例平分显存,6个paddlex实例分两组分别指向两个vllm实例。下面是单卡内部完整链路:
┌─────────────────────────────────────────────┐
│ Uvicorn 网关 :9101 │
│ 48个 upstreams 中包括本卡的6个paddlex │
└─────┬──────┬──────┬──────┬──────┬──────┬─────┘
│ │ │ │ │ │
┌────────▼──┐ ┌─▼──────┐┌─▼──────┐┌─▼──────┐┌─▼──┐ ┌─▼──────────┐
│paddlex │ │paddlex ││paddlex ││paddlex ││pad │ │paddlex │
│:9120 │ │:9130 ││:9140 ││:9150 ││:.. │ │:9170 │
│paddle- │ │paddle- ││paddle- ││paddle- ││ │ │paddle- │
│9120-npu-2 │ │9130-.. ││9140-.. ││9150-.. ││ │ │9170-npu-2 │
└─────┬─────┘ └───┬────┘└───┬────┘└───┬────┘└──┬─┘ └──────┬─────┘
│ │ │ │ │ │
│ 前3个实例 │ │ 后3个实例│ │ │
│ ┌────────┴─────────┴──┐ ┌───┴───────┴────────────┴──┐
│ │ │ │ │
└──► pipeline_config │ │ pipeline_config │
│ server_url → :9025 │ │ server_url → :9035 │
│ │ │ │
└──────────┬──────────┘ └─────────────┬──────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ vllm-ascend │ │ vllm-ascend │
│ :9025 (实例1) │ │ :9035 (实例2) │
│ gpu-mem-util 0.45 │ │ gpu-mem-util 0.45 │
│ 容器: vllm-ascend-│ │ 容器: vllm-ascend-│
│ paddleocr-9025- │ │ paddleocr-9035- │
│ npu-2 │ │ npu-2 │
└────────┬─────────┘ └────────┬─────────┘
│ │
└────────┬───────────────┘
│
┌─────────▼─────────┐
│ NPU 卡2 │
│ /dev/davinci2 │
│ Ascend 910B │
└───────────────────┘
8卡完整端口矩阵与路由对照表
每一行是一条paddlex→vllm路由绑定关系,端口号本身就编码了归属信息(81xx=vllm, 82~87xx=paddlex)。
卡号 │ paddlex 6实例端口 │ vllm 2实例端口 │ vllm实例1 容器 │ vllm实例2 容器
─────┼────────────────────────┼────────────────────────┼───────────────────────────┼───────────────────────────
卡0 │ 9126 9136 9146 │ 9031 (实例1) │ ...-9031-npu-0 │ ...-9041-npu-0
│ 9156 9166 9176 │ 9041 (实例2) │ │
─────┼────────────────────────┼────────────────────────┼───────────────────────────┼───────────────────────────
卡1 │ 9127 9137 9147 │ 9032 (实例1) │ ...-9032-npu-1 │ ...-9042-npu-1
│ 9157 9167 9177 │ 9042 (实例2) │ │
─────┼────────────────────────┼────────────────────────┼───────────────────────────┼───────────────────────────
卡2 │ 9120 9130 9140 │ 9025 (实例1) ✅已有 │ ...-9025-npu-2 ✅已有 │ ...-9035-npu-2 新增
│ 9150 9160 9170 │ 9035 (实例2) 新增 │ │
─────┼────────────────────────┼────────────────────────┼───────────────────────────┼───────────────────────────
卡3 │ 9121 9131 9141 │ 9026 (实例1) ✅已有 │ ...-9026-npu-3 ✅已有 │ ...-9036-npu-3 新增
│ 9151 9161 9171 │ 9036 (实例2) 新增 │ │
─────┼────────────────────────┼────────────────────────┼───────────────────────────┼───────────────────────────
卡4 │ 9122 9132 9142 │ 9027 (实例1) ✅已有 │ ...-9027-npu-4 ✅已有 │ ...-9037-npu-4 新增
│ 9152 9162 9172 │ 9037 (实例2) 新增 │ │
─────┼────────────────────────┼────────────────────────┼───────────────────────────┼───────────────────────────
卡5 │ 9123 9133 9143 │ 9028 (实例1) ✅已有 │ ...-9028-npu-5 ✅已有 │ ...-9038-npu-5 新增
│ 9153 9163 9173 │ 9038 (实例2) 新增 │ │
─────┼────────────────────────┼────────────────────────┼───────────────────────────┼───────────────────────────
卡6 │ 9124 9134 9144 │ 9029 (实例1) ✅已有 │ ...-9029-npu-6 ✅已有 │ ...-9039-npu-6 新增
│ 9154 9164 9174 │ 9039 (实例2) 新增 │ │
─────┼────────────────────────┼────────────────────────┼───────────────────────────┼───────────────────────────
卡7 │ 9125 9135 9145 │ 9030 (实例1) ✅已有 │ ...-9030-npu-7 ✅已有 │ ...-9040-npu-7 新增
│ 9155 9165 9175 │ 9040 (实例2) 新增 │ │
─────┴────────────────────────┴────────────────────────┴───────────────────────────┴───────────────────────────
总计: 16 vllm-ascend 实例 + 48 paddlex 实例,分布在 8 张 Ascend 910B NPU 卡上
命名规则: vllm-ascend-paddleocr-{端口}-npu-{卡号} / paddle-{端口}-npu-{卡号}
完整拓扑可以概括为:Uvicorn网关轮询48个paddlex → 每6个paddlex共享一张NPU卡 → 卡上2个vllm各自服务3个paddlex → vllm通过Ascend 910B NPU完成模型推理。扩容时只需将新实例如端口号加入 config.json 的 upstreams 列表,网关即可自动纳入轮询,无需重启其他服务。
推理引擎层:vllm-ascend
vllm-ascend 是基于华为昇腾NPU实现的高性能推理引擎,承载 PaddleOCR-VL-1.5-0.9B 模型的加载和推理。每个实例监听一个独立端口,通过 OpenAI 兼容的 `/v1/chat/completions` 接口与上层 paddlex 交互。
单卡双实例的关键参数 `--gpu-memory-utilization 0.45` 确保两个vllm实例平分一张卡的显存。
OCR处理层:paddlex
paddlex 是 PaddleOCR 的完整 Pipeline 服务,负责:
- **文档预处理**(方向校正、去扭曲)
- **版面检测**(PP-DocLayoutV3)
- **VL识别**(调用vllm完成实际OCR)
- **Markdown格式化**(版面重排、表格/公式处理)
paddlex 通过 `server_url: http://127.0.0.1:{vllm端口}/v1` 指向对应的vllm实例。
负载均衡层:uvicorn网关
FastAPI 服务运行在 `paddle-10020-npu-2` 容器内的 9101 端口,对所有 48 个 paddlex 实例做 **Round-Robin 轮询** + **failover容错**,统一对外提供 `/ocr` 和 `/ocr-file` 两个API。
四、完整端口规划
vllm-ascend(每卡2个,共16个)
NPU卡 vllm实例1 vllm实例2
卡0 9031 (新增) 9041 (新增)
卡1 9032 (新增) 9042 (新增)
卡2 9025 (已有) 9035 (新增)
卡3 9026 (已有) 9036 (新增)
卡4 9027 (已有) 9037 (新增)
卡5 9028 (已有) 9038 (新增)
卡6 9029 (已有) 9039 (新增)
卡7 9030 (已有) 9040 (新增)
paddlex(每卡6个,共48个)
NPU卡 实例1 实例2 实例3 实例4 实例5 实例6
卡0 9126 9136 9146 9156 9166 9176
卡1 9127 9137 9147 9157 9167 9177
卡2 9120 9130 9140 9150 9160 9170
卡3 9121 9131 9141 9151 9161 9171
卡4 9122 9132 9142 9152 9162 9172
卡5 9123 9133 9143 9153 9163 9173
卡6 9124 9134 9144 9154 9164 9174
卡7 9125 9135 9145 9155 9165 9175
paddlex → vllm 路由规则
| NPU卡 | vllm实例1 | vllm实例2 |
|---|---|---|
| 卡0 | 9031 (新增) | 9041 (新增) |
| 卡1 | 9032 (新增) | 9042 (新增) |
| 卡2 | 9025 (已有) | 9035 (新增) |
| 卡3 | 9026 (已有) | 9036 (新增) |
| 卡4 | 9027 (已有) | 9037 (新增) |
| 卡5 | 9028 (已有) | 9038 (新增) |
| 卡6 | 9029 (已有) | 9039 (新增) |
| 卡7 | 9030 (已有) | 9040 (新增) |
| NPU卡 | 实例1 | 实例2 | 实例3 | 实例4 | 实例5 | 实例6 |
|---|---|---|---|---|---|---|
| 卡0 | 9126 | 9136 | 9146 | 9156 | 9166 | 9176 |
| 卡1 | 9127 | 9137 | 9147 | 9157 | 9167 | 9177 |
| 卡2 | 9120 | 9130 | 9140 | 9150 | 9160 | 9170 |
| 卡3 | 9121 | 9131 | 9141 | 9151 | 9161 | 9171 |
| 卡4 | 9122 | 9132 | 9142 | 9152 | 9162 | 9172 |
| 卡5 | 9123 | 9133 | 9143 | 9153 | 9163 | 9173 |
| 卡6 | 9124 | 9134 | 9144 | 9154 | 9164 | 9174 |
| 卡7 | 9125 | 9135 | 9145 | 9155 | 9165 | 9175 |
paddlex → vllm 路由规则
每卡前3个paddlex实例指向vllm实例1,后3个指向实例2:
| NPU卡 | → vllm实例1 | → vllm实例2 |
|---|---|---|
| 卡0 | 9126/9136/9146 → 9031 | 9156/9166/9176 → 9041 |
| 卡1 | 9127/9137/9147 → 9032 | 9157/9167/9177 → 9042 |
| 卡2 | 9120/9130/9140 → 9025 | 9150/9160/9170 → 9035 |
| 卡3 | 9121/9131/9141 → 9026 | 9151/9161/9171 → 9036 |
| 卡4 | 9122/9132/9142 → 9027 | 9152/9162/9172 → 9037 |
| 卡5 | 9123/9133/9143 → 9028 | 9153/9163/9173 → 9038 |
| 卡6 | 9124/9134/9144 → 9029 | 9154/9164/9174 → 9039 |
| 卡7 | 9125/9135/9145 → 9030 | 9155/9165/9175 → 9040 |
五、vllm-ascend 推理引擎部署
5.1 启动脚本
每个vllm实例对应一个启动脚本 `vllm-start-{端口}.sh`:
#!/bin/bash
export TASK_QUEUE_ENABLE=1
export CPU_AFFINITY_CONF=1
export PYTORCH_NPU_ALLOC_CONF="expandable_segments:True"
export VLLM_USE_MODELSCOPE=true
export MODEL_PATH="/data/PaddleOCR-VL-1.5-0.9B"
vllm serve ${MODEL_PATH} \
--max-num-batched-tokens 16384 \
--served-model-name PaddleOCR-VL-1.5-0.9B \
--port __PORT__ \
--trust-remote-code \
--no-enable-prefix-caching \
--mm-processor-cache-gb 0 \
--gpu-memory-utilization 0.45 \
--compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY"}' \
--additional_config '{"enable_cpu_binding":true}'
关键参数说明:
| 参数 | 值 | 作用 |
|---|---|---|
| `--port` | 9025~9042 | 每个实例独立端口 |
| `--gpu-memory-utilization` | 0.45 | 单卡双实例,平分显存 |
| `--max-num-batched-tokens` | 16384 | 最大批处理token数 |
| `--mm-processor-cache-gb` | 0 | 关闭多模态处理器缓存 |
| `--no-enable-prefix-caching` | — | 关闭前缀缓存(OCR场景无益) |
| `expandable_segments` | True | 动态显存分配(PyTorch NPU特性) |
5.2 容器创建
通过 `setup.sh config` 自动批量生成所有端口的启动脚本,容器创建由 Docker Compose 接管(见第8节)。
5.3 模型加载验证
vllm启动后等待2-5分钟模型加载,可通过API测试验证:
curl http://127.0.0.1:9031/v1/models | python3 -m json.tool
六、paddle + paddlex OCR处理层部署
6.1 Pipeline配置文件
每个paddlex实例对应一个 `pipeline_config_vllm-{端口}.yaml`,核心配置如下:
pipeline_name: PaddleOCR-VL-1.5
batch_size: 128
use_queues: True
use_doc_preprocessor: True # 文档预处理
use_layout_detection: True # 版面检测
use_chart_recognition: False
use_seal_recognition: False
format_block_content: False
merge_layout_blocks: True
SubModules:
LayoutDetection:
module_name: layout_detection
model_name: PP-DocLayoutV3
model_dir: /root/PP-DocLayoutV3
batch_size: 32
VLRecognition:
module_name: vl_recognition
model_name: PaddleOCR-VL-1.5-0.9B
batch_size: 16384
genai_config:
backend: vllm-server
server_url: http://127.0.0.1:9031/v1 # ← 指向对应vllm实例
SubPipelines:
DocPreprocessor:
pipeline_name: doc_preprocessor
batch_size: 16
use_doc_orientation_classify: True
use_doc_unwarping: True
SubModules:
DocOrientationClassify:
model_name: PP-LCNet_x1_0_doc_ori
model_dir: /data/PP-LCNet_x1_0_doc_ori
DocUnwarping:
model_name: UVDoc
model_dir: /data/UVDoc
注意 `server_url` 必须指向同卡对应的vllm实例,这是paddlex与vllm之间的唯一纽带。
6.2 Batch Size优化
原有配置 batch_size 较小,扩容后根据每卡实例数进行了调整:
sed -i "s/batch_size: 8/batch_size: 16/" pipeline_config_vllm*
sed -i "s/batch_size: 64/batch_size: 128/" pipeline_config_vllm*
sed -i "s/batch_size: 4096/batch_size: 8192/" pipeline_config_vllm*
6.3 服务启动
paddlex通过 `docker exec` 在容器内后台启动:
docker exec -d paddle-10026-npu-0 bash -c \
"source ~/.bashrc && paddlex --serve --device npu --port 9126 \
--pipeline /data/pipeline_config_vllm-9126.yaml \
> /data/paddle-9126.log 2>&1 & wait"
关键参数:
| 参数 | 说明 |
|---|---|
| `--serve` | 以HTTP服务模式运行 |
| `--device npu` | 使用NPU设备 |
| `--port` | paddlex服务端口,与容器名中的端口一致 |
| `--pipeline` | 指向对应的pipeline配置文件 |
七、负载均衡网关
所有 paddlex 实例前面加了一层基于 FastAPI + uvicorn 的负载均衡网关,负责将外部请求均匀分发到48个上游实例。
核心能力
- Round-Robin 轮询:对48个上游实例逐一循环分发,保证负载均匀
- Failover 容错:单次请求最多尝试12个上游,当某实例故障时自动切换到下一个
- 统一API:提供
POST /ocr(JSON入参,支持base64/URL/local_path)和 POST /ocr-file(multipart文件上传)
- OCR后处理:去除图片标签、清理识别噪点、合并空白行
- 连接池复用:基于 httpx.AsyncClient 的 HTTP 连接池,避免反复建连开销
配置驱动
POST /ocr(JSON入参,支持base64/URL/local_path)和 POST /ocr-file(multipart文件上传)网关通过 config.json 驱动,核心配置项:
{
"upstreams": [
"http://127.0.0.1:9120", "http://127.0.0.1:9121",
// ... 共48个 paddlex 实例
"http://127.0.0.1:9177"
],
"load_balance": {
"mode": "round_robin", // round_robin 或 random
"max_attempts": 12 // 单次请求最多尝试的上游数
},
"http": {
"timeout_s": 300, // OCR单次请求可能耗时较长
"pool_max_connections": 200,
"pool_max_keepalive_connections": 100
},
"postprocess": {
"strip_images": true, // 去除结果中的图片Markdown标签
"remove_repeated_hui": { // 清理签名区识别噪点(连续"回"字)
"enabled": true,
"min_repeats": 2
}
}
}
Failover 流程
网关与每个 paddlex 实例的交互需要两级调用:/layout-parsing 完成版面解析,/restructure-pages 完成 Markdown 重组。当某个上游发生超时或错误时,网关自动跳过该实例,尝试序列中的下一个。只有所有 max_attempts 次尝试均失败后才返回 502。
客户端调用示例
# 方式1: base64 编码
import requests, base64
with open("doc.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
r = requests.post("http://127.0.0.1:9101/ocr",
json={"base64": b64}, timeout=300)
print(r.json()["text"])
# 方式2: 文件上传
r = requests.post("http://127.0.0.1:9101/ocr-file",
files={"file": open("doc.png", "rb")}, timeout=300)
print(r.json()["text"])
验证负载均衡
# 方式1: base64 编码
import requests, base64
with open("doc.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
r = requests.post("http://127.0.0.1:9101/ocr",
json={"base64": b64}, timeout=300)
print(r.json()["text"])
# 方式2: 文件上传
r = requests.post("http://127.0.0.1:9101/ocr-file",
files={"file": open("doc.png", "rb")}, timeout=300)
print(r.json()["text"])连续请求观察返回的 upstream 字段是否轮询变化:
for i in {1..8}; do
curl -s -X POST http://127.0.0.1:9101/ocr -H "Content-Type: application/json" -d '{"local_path":"./test.png"}' | python -c 'import sys,json; d=json.load(sys.stdin); print(d.get("upstream"))'
done
网关核心代码基于 FastAPI + httpx 实现,不到400行。新增实例只需加到 config.json 的 upstreams 列表中即可自动纳入轮询,无需重启其他服务。
八、Docker Compose 编排
由于服务器无法使用K8s,整个集群通过 Docker Compose 编排管理。分为两组compose文件:
8.1 all-docker-compose.vllm.yml
16个vllm容器定义。以卡0双实例为例:
version: "3.7"
x-vllm-common: &vllm-common
image: quay.io/ascend/vllm-ascend:v0.19.1rc1
restart: unless-stopped
network_mode: host # 直接使用宿主机网络
privileged: true # NPU设备访问需要特权模式
user: "0"
shm_size: "100g"
devices: # NPU管理设备
- /dev/davinci_manager
- /dev/devmm_svm
- /dev/hisi_hdc
volumes: # NPU驱动 + 数据挂载
- /usr/local/dcmi:/usr/local/dcmi
- /usr/local/bin/npu-smi:/usr/local/bin/npu-smi
- /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/
- /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info
- /etc/ascend_install.info:/etc/ascend_install.info
- /opt/its/model/PaddleOCR:/data/
services:
vllm-9031-npu-0:
<<: *vllm-common
container_name: vllm-ascend-paddleocr-9031-npu-0
devices:
- /dev/davinci0 # ← 绑定卡0
environment:
ASCEND_RT_VISIBLE_DEVICES: "0"
command: bash -c "/data/vllm-start-9031.sh &> /data/vllm-9031.log & wait"
vllm-9041-npu-0:
<<: *vllm-common
container_name: vllm-ascend-paddleocr-9041-npu-0
devices:
- /dev/davinci0 # ← 与9031共享卡0
environment:
ASCEND_RT_VISIBLE_DEVICES: "0"
command: bash -c "/data/vllm-start-9041.sh &> /data/vllm-9041.log & wait"
关键设计点:
1. **YAML锚点复用**:`x-vllm-common: &vllm-common` 定义公共配置,所有服务通过 `<<: *vllm-common` 继承,大幅减少重复代码 2. **network_mode: host**:NPU推理服务直接使用宿主机端口,无需端口映射 3. **单卡双实例**:两个容器都绑定同一张NPU卡(如 `/dev/davinci0`),通过 `--gpu-memory-utilization 0.45` 实现显存平分 4. **command**:容器启动时自动执行启动脚本,vllm服务作为容器主进程运行
8.2 all-docker-compose.paddle.yml
48个paddle容器定义,结构与vllm类似。差异点:
x-paddle-common: &paddle-common
image: paddleocrvl:20260529
restart: unless-stopped
network_mode: host
privileged: true
shm_size: "128G" # paddle需要更大共享内存
volumes:
- /usr/local/Ascend/driver:/usr/local/Ascend/driver
- /opt/its/model/PaddleOCR:/data/ # 配置文件 + 模型 + 脚本
command: tail -f /dev/null # 容器保持运行,paddlex通过exec启动
注意 paddle 容器的 `command: tail -f /dev/null` —— 容器保持存活但不自动启动paddlex服务,paddlex通过外部 `docker exec` 命令在容器内手动启动。
这是因为 paddle 容器内需要先 `source ~/.bashrc` 初始化NPU环境变量,且paddlex服务的启动依赖于对应vllm实例已就绪。
九、一键部署与运维
9.1 setup.sh 自动化部署
部署脚本 `setup.sh` 提供完整的自动化部署流程:
bash setup.sh all # 一键完整部署
执行过程:
检查端口占用
↓
生成 vllm 启动脚本(16个)
↓
生成 paddle 配置文件(48个)
↓
Docker Compose 启动 vllm 容器
↓
等待模型加载完成(人工确认)
↓
Docker Compose 启动 paddle 容器
↓
docker exec 在每个容器内启动 paddlex 服务
↓
更新负载均衡配置 → 重启 uvicorn
也支持分步执行:
bash setup.sh check # 检查端口
bash setup.sh config # 生成配置
bash setup.sh vllm # 启动vllm
bash setup.sh paddle # 启动paddle
bash setup.sh lb # 更新负载均衡
bash setup.sh verify # 验证所有服务
9.2 Pipeline配置文件自动生成
# 核心逻辑:复制模板 + 替换server_url指向对应vllm
gen_one() {
local P_PORT=$1 V_PORT=$2
cp pipeline_config_vllm.yaml pipeline_config_vllm-${P_PORT}.yaml
sed -i "s/127.0.0.1:9000/127.0.0.1:${V_PORT}/g" \
pipeline_config_vllm-${P_PORT}.yaml
}
# 卡0:前3→9031,后3→9041
gen_one 9126 9031; gen_one 9136 9031; gen_one 9146 9031
gen_one 9156 9041; gen_one 9166 9041; gen_one 9176 9041
# ... 依此类推,共48个配置
9.3 负载均衡配置自动更新
# 核心逻辑:复制模板 + 替换server_url指向对应vllm
gen_one() {
local P_PORT=$1 V_PORT=$2
cp pipeline_config_vllm.yaml pipeline_config_vllm-${P_PORT}.yaml
sed -i "s/127.0.0.1:9000/127.0.0.1:${V_PORT}/g" \
pipeline_config_vllm-${P_PORT}.yaml
}
# 卡0:前3→9031,后3→9041
gen_one 9126 9031; gen_one 9136 9031; gen_one 9146 9031
gen_one 9156 9041; gen_one 9166 9041; gen_one 9176 9041
# ... 依此类推,共48个配置`setup.sh lb` 通过 `docker exec` 在网关容器内更新配置并重启服务:
# 1. 写入新的 config.json(48个upstream)
# 2. 自动备份旧配置为 config.json.bak.{时间戳}
# 3. 查找并杀掉旧的 uvicorn 进程
# 4. 重新启动 uvicorn
9.4 ops.sh 运维工具
bash ops.sh status # 查看所有容器状态
bash ops.sh ports # 批量检查所有端口
bash ops.sh restart-vllm 9031 # 重启指定vllm
bash ops.sh restart-paddle 9126 # 重启指定paddle
bash ops.sh log-vllm 9031 100 # 查看vllm日志
bash ops.sh log-paddle 9126 100 # 查看paddle日志
bash ops.sh test-vllm 9035 # 测试vllm接口
bash ops.sh test-paddle 9126 # 测试paddle接口
bash ops.sh npu # 查看NPU状态
bash ops.sh start-all # 一键启动所有已停止的容器
bash ops.sh up-vllm # Compose批量操作
bash ops.sh down-paddle
十、设计决策与经验总结
为什么不能直接用Nginx做负载均衡?
bash ops.sh status # 查看所有容器状态
bash ops.sh ports # 批量检查所有端口
bash ops.sh restart-vllm 9031 # 重启指定vllm
bash ops.sh restart-paddle 9126 # 重启指定paddle
bash ops.sh log-vllm 9031 100 # 查看vllm日志
bash ops.sh log-paddle 9126 100 # 查看paddle日志
bash ops.sh test-vllm 9035 # 测试vllm接口
bash ops.sh test-paddle 9126 # 测试paddle接口
bash ops.sh npu # 查看NPU状态
bash ops.sh start-all # 一键启动所有已停止的容器
bash ops.sh up-vllm # Compose批量操作
bash ops.sh down-paddleNginx确实能做HTTP反向代理和轮询,但在OCR场景有几个问题:
1. **两级API调用**:OCR需要先调 `/layout-parsing` 再调 `/restructure-pages`,Nginx无法串联 2. **后处理需求**:OCR结果需要strip images、去除"回"字噪点等定制化后处理 3. **灵活的入参支持**:需要同时支持URL/base64/data_url/local_path多种输入方式 4. **精细的failover控制**:需要返回具体哪个上游失败、重试了多少次
用FastAPI自研网关,代码不到400行就完美解决了这些问题。
为什么paddlex不随容器自动启动?
paddlex需要先加载模型,而模型加载依赖vllm已就绪。如果随容器自动启动,时序不可控。通过 `docker exec` 外部控制,可以等vllm全部就绪后再逐批启动paddlex。
单卡多实例的GPU内存规划
vllm单卡双实例的核心参数:
--gpu-memory-utilization 0.45
每个实例最多使用45%的NPU显存,两个实例合计90%,留10%给系统开销。这是经过实际压测验证的值——太低浪费算力,太高会OOM。
paddlex的batch_size也做了对应调整:从原本8/64/4096提升到16/128/8192,充分利用单卡跑多个实例的剩余算力。
命名规范的工程价值
整个项目有统一的命名规范:
规则 示例
vllm容器: vllm-ascend-paddleocr-{端口}-npu-{卡号} vllm-ascend-paddleocr-9031-npu-0
paddle容器: paddle-{端口}-npu-{卡号} paddle-10026-npu-0
启动脚本: vllm-start-{端口}.sh vllm-start-9031.sh
配置文件: pipeline_config_vllm-{端口}.yaml pipeline_config_vllm-9126.yaml
端口号本身就编码了类型和归属信息:
- 81xx → vllm端口
- 82xx/83xx/84xx/85xx/86xx/87xx → paddle端口,前两位区分实例编号
这让自动化脚本可以简单地通过端口号推导出所有关联文件的路径。
结语
从最初的6个vllm+12个paddle,到最终的16个vllm+48个paddle集群,这套方案经历了从手动 `docker run` 到 Docker Compose 编排,从静态轮询到自研uvicorn智能网关的完整演进。
核心代码 `ocr_backend.py` 不到400行,配合 `config.json` 驱动,实现了:
- 48个上游实例的Round-Robin负载均衡
- 请求级failover容错
- 灵活的图片输入方式(URL/base64/local_path/multipart)
- 完善的OCR后处理Pipeline
- 可动态扩展(新增实例只需加到config.json)
整套部署实现了全自动化(`bash setup.sh all`),从零到可用只需一条命令。
简记。







