最近在一台双 Intel Arc A770 16GB 的机器上折腾 PaddleOCR-VL-1.6。
一开始我的想法很简单:PaddleOCR 已经有 Intel GPU 镜像了,A770 也是 Intel GPU,那直接 Docker 跑起来应该就行。
结果并没有。
实际一路碰到了:
- 官方镜像 ENTRYPOINT 不对,启动后莫名其妙加载 Qwen;
- BF16 路径报错;
- IPEX Paged Attention 不支持 A770;
- 换新版 vLLM 后,WSL2 下 oneCCL/XCCL 又直接 Segmentation Fault;
- 修完 XCCL,FlashAttention 又提示只支持 XE2/XE3;
- 最后把 Decoder 和 Vision Encoder 的 Attention Backend 拆开,才真正把 PaddleOCR-VL 跑起来;
- 接 PaddleOCR Pipeline 时,又碰到了模型名不一致导致的 404。
这篇文章不打算写成"标准安装教程",主要把我实际怎么排问题、为什么换方案、最后用了什么配置记录下来。
如果你也是:
Windows 11
WSL2
Intel Arc A770 16GB
Docker
PaddleOCR-VL-1.6
这篇应该能省不少时间。
先说最后跑通的方案
最后我没有继续用 PaddleOCR 官方镜像里面那套旧 vLLM + IPEX 推理链,而是改成:
makefile
Windows 11
↓
WSL2 Ubuntu 24.04
↓
Intel Arc A770 16GB
↓
/dev/dxg
↓
Docker
↓
PyTorch 2.13.0+xpu
↓
vLLM 0.27.1
↓
Decoder: TRITON_ATTN
Vision: TORCH_SDPA
↓
PaddleOCR-VL-1.6
我这台机器有两张 A770,最后的分工是:
bash
A770 #0
└── PaddleOCR Pipeline
└── PP-DocLayoutV3 / 预处理等
A770 #1
└── vLLM
└── PaddleOCR-VL-1.6
如果你只有一张卡,也可以跑,只是 Pipeline 和 VLM 会一起抢显存。
1. 先确认 WSL2 真能看到 A770
我的环境是:
Windows 11
WSL2
Ubuntu 24.04
Intel Arc A770 16GB
Docker CE
先看 WSL 有没有 GPU 设备:
bash
ls -l /dev/dxg
只要 /dev/dxg 存在,说明 Windows 已经把 GPU 暴露给 WSL 了。
不过这里有个容易误判的地方:
/dev/dxg存在,只能说明设备通了,不代表后面的 Intel XPU Kernel 都支持 A770。
这个区别后面很重要。
我在 XPU 容器里测试:
scss
import torch
print(torch.xpu.is_available())
print(torch.xpu.device_count())
print(torch.xpu.get_device_properties(0))
A770 能正常识别:
scss
Intel(R) Graphics [0x56a0]
Architecture: intel_gpu_acm_g10
Memory: 15930MB
Compute Units: 512
这里的:
intel_gpu_acm_g10
就是 A770 的 ACM-G10,也就是 Alchemist / Xe-HPG。
到这里至少可以确定一件事:
驱动、WSL、Docker 到 GPU 这一层是通的。
2. 第一个坑:官方镜像启动后为什么是 Qwen?
一开始直接用 PaddleOCR 官方 Intel GPU 镜像。
结果容器是起来了,但日志不对。
我预期应该加载:
PaddleOCR-VL-1.6
结果日志里出现的是:
Qwen/Qwen3-0.6B
这时候第一反应可能是模型参数传错了。
后来我直接 inspect 镜像:
arduino
docker image inspect \
ccr-2vdh3abv-pub.cnc.bj.baidubce.com/paddlepaddle/paddleocr-genai-vllm-server:latest-intel-gpu \
--format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}'
看到的是类似这种结构:
ini
Entrypoint=["bash","-c","python3 -m vllm.entrypoints.openai.api_server"]
Cmd=["/bin/bash","-c","paddleocr genai_server ..."]
问题基本就出来了。
镜像本来希望执行的是:
erlang
paddleocr genai_server ...
但 ENTRYPOINT 本身已经写死成:
python3 -m vllm.entrypoints.openai.api_server
所以最终启动行为和预期不一致。
我的处理方式是直接覆盖 ENTRYPOINT。
Docker 里:
bash
--entrypoint /bin/bash
Compose 里也可以:
ini
entrypoint: []
然后自己显式执行真正想跑的命令。
这个问题解决后,模型终于不再莫名其妙跑到 Qwen 上了。
3. 第二个坑:BF16 报错
模型终于加载对了,接着又报:
makefile
RuntimeError: IPEX SDP only supports half datatype
当时跑的是旧 Intel 镜像里的 IPEX 路径。
所以先别折腾别的,直接把 dtype 改成 FP16:
css
--dtype half
也就是:
torch.float16
如果走配置文件:
makefile
dtype: half
gpu-memory-utilization: 0.7
改完以后,这个错误就过去了。
这里我不建议写成"A770 不支持 BF16"。
更准确一点应该是:
我当时使用的那版 IPEX SDP 实现,在 A770 这条 Attention 路径上要求 FP16。
这是软件实现限制和硬件架构共同作用的结果,没必要扩大成整个 A770 都"不支持 BF16"。
4. FP16 改完以后,又撞上 Paged Attention
我原本以为改完 FP16 应该就能跑了。
结果又来了一个:
Unsupported gpu_arch of paged_attention_vllm!!
这个错误就比前面的麻烦了。
因为这时候:
- GPU 能识别;
- 模型能加载;
- FP16 也已经生效;
- vLLM 已经进入真正的推理初始化。
所以我开始往底层找。
最后定位到:
bash
/usr/local/lib/python3.12/dist-packages/
intel_extension_for_pytorch/lib/libxetla_sdpa.so
里面能找到:
paged_attention_vllm
相关实现。
到这里我基本就不继续怀疑 WSL 或 Docker 了。
问题更像是:
markdown
旧版 IPEX
↓
XeTLA SDPA / Paged Attention
↓
A770 / ACM-G10
不兼容。
也就是说,即使前面的环境全部正常,这个 kernel 本身还是过不去。
这时候继续死磕官方旧镜像意义就不大了,所以我决定直接换新版 vLLM XPU。
5. 换 vLLM 0.27.1 XPU
后来换成:
bash
vllm/vllm-openai-xpu:v0.27.1
先拉镜像:
bash
docker pull vllm/vllm-openai-xpu:v0.27.1
然后直接进容器测试 A770:
ini
docker run --rm -it \
--user root \
--device /dev/dxg:/dev/dxg \
-v /usr/lib/wsl:/usr/lib/wsl:ro \
--cap-add SYS_PTRACE \
--security-opt seccomp=unconfined \
--ipc=host \
-e ZE_FLAT_DEVICE_HIERARCHY=COMPOSITE \
-e ZE_AFFINITY_MASK=0 \
--entrypoint /bin/bash \
vllm/vllm-openai-xpu:v0.27.1
容器里:
scss
import torch
import vllm
print(torch.__version__)
print(vllm.__version__)
print(torch.xpu.is_available())
print(torch.xpu.get_device_properties(0))
我的结果:
yaml
torch: 2.13.0+xpu
vllm: 0.27.1
xpu available: True
Intel(R) Graphics [0x56a0]
这版和前面的一个很大区别是:
已经不再依赖旧的 intel_extension_for_pytorch 那套 Paged Attention 路径。
整个链路变成:
vLLM
↓
vllm-xpu-kernels
↓
PyTorch XPU
↓
Level Zero
↓
A770
当时我以为终于结束了。
结果还有坑。
6. WSL2 下 oneCCL/XCCL 直接 Segmentation Fault
新版 vLLM 起到一半,直接:
vbnet
failed to get PCI device
Segmentation fault
继续往下看堆栈,落到了:
arduino
ProcessGroupXCCL::allreduce
后来定位到:
bash
/opt/venv/lib/python3.12/site-packages/vllm/v1/worker/xpu_worker.py
里面有这么一段:
scss
if torch.distributed.is_xccl_available():
torch.distributed.all_reduce(torch.zeros(1).xpu())
问题是我现在明明是单卡:
ini
world_size = 1
它还是会做一次 all_reduce warmup。
原生 Linux 上可能没什么,但我这里是 WSL2 + /dev/dxg,到了 XCCL / oneCCL 做 PCI 设备相关初始化的时候直接炸了。
单卡本身又不需要跨卡 all_reduce,所以我直接改成:
scss
if (
torch.distributed.is_xccl_available()
and self.parallel_config.world_size > 1
):
torch.distributed.all_reduce(torch.zeros(1).xpu())
也就是:
objectivec
单卡
→ 不做 XCCL all_reduce
多卡
→ 保留原来的 warmup
Patch 完以后,vLLM 终于越过了这一段。
这里我更愿意把它称为:
WSL2 + 单 XPU 场景下的 XCCL warmup 兼容问题。
至于 oneCCL 内部为什么没正确处理这个 PCI 设备,我没有继续深挖,因为这个 all_reduce 对单卡本来就没有意义。
7. 修完 XCCL,FlashAttention 又不支持 A770
这次模型继续往后启动。
然后:
csharp
Only XE2/XE3 cutlass kernel is supported currently.
看到这里其实已经很好判断了。
A770 是:
Xe-HPG / Alchemist
而当前命中的这个 XPU FlashAttention kernel 明确只接受:
XE2/XE3
所以这条路还是不能走。
我后来做了两次尝试。
先试把整个 Attention 都改成 Triton。
结果 PaddleOCR-VL 的视觉部分又不接受这种组合。
最后才确定下来:
sql
Language Decoder
→ TRITON_ATTN
Vision Encoder
→ TORCH_SDPA
对应参数:
css
--attention-backend TRITON_ATTN
--mm-encoder-attn-backend TORCH_SDPA
另外我还加了:
ini
VLLM_TRITON_USE_TD=0
这套组合最后能正常走过模型加载、KV Cache 初始化和多模态 warmup。
8. 真正跑起来的 vLLM 命令
我当前能跑的核心命令是:
css
vllm serve PaddlePaddle/PaddleOCR-VL-1.6 \
--trust-remote-code \
--dtype half \
--max-model-len 8192 \
--max-num-batched-tokens 8192 \
--no-enable-prefix-caching \
--mm-processor-cache-gb 0 \
--gpu-memory-utilization 0.7 \
--attention-backend TRITON_ATTN \
--mm-encoder-attn-backend TORCH_SDPA \
--host 0.0.0.0 \
--port 8000
环境变量:
ini
VLLM_TRITON_USE_TD=0
启动成功后,我这张 16GB A770 能看到类似:
yaml
Available KV cache memory: 10.56 GiB
GPU KV cache size: 615,120 tokens
Maximum concurrency for 8,192 tokens per request: 75.09x
这里顺便说一下这个 75.09x。
不要把它理解成 A770 可以同时高性能跑 75 个 OCR 请求。
这个数字更接近:
KV Cache token 容量
÷
max_model_len
得到的 sequence cache capacity 估算。
真实业务并发还会被这些东西影响:
图片大小
Vision Encoder
Prefill
Triton Kernel
Batch
CPU 预处理
Layout 模型
实际输出 token
所以最终能跑多少并发,还是得自己压测。
9. 关于 --served-model-name,这里也踩了坑
PaddleOCR Pipeline 默认请求的是:
PaddleOCR-VL-1.6-0.9B
但我这样启动:
vllm serve PaddlePaddle/PaddleOCR-VL-1.6
vLLM 暴露出来的模型名是:
PaddlePaddle/PaddleOCR-VL-1.6
所以 Pipeline 调底层 vLLM 时直接 404:
go
The model `PaddleOCR-VL-1.6-0.9B` does not exist.
我一开始也很自然地加了:
css
--served-model-name PaddleOCR-VL-1.6-0.9B
理论上这确实是 vLLM 用来改 API 模型名的参数。
但在我当前 A770 + vLLM XPU 环境里,加完以后有一次模型长时间卡在:
arduino
Compile and warming up model for size 8192
EngineCore CPU 一直 100%,十几分钟没有继续。
而不加这个参数时,之前是正常启动过的。
所以我现在的处理方式是:
vLLM 不改名字。
继续保留:
PaddlePaddle/PaddleOCR-VL-1.6
然后让上层 PaddleOCR Pipeline 请求这个实际存在的 API 模型名。
目前我的 Pipeline 配置是:
yaml
VLRecognition:
module_name: vl_recognition
model_name: PaddleOCR-VL-1.6-0.9B
model_dir: null
batch_size: 4096
genai_config:
backend: vllm-server
server_url: http://paddleocr-vllm-a770:8000/v1
client_kwargs:
model_name: PaddlePaddle/PaddleOCR-VL-1.6
这里其实有两个"模型名":
PaddleOCR Pipeline 自己的模块配置名
PaddleOCR-VL-1.6-0.9B
vLLM API 真正提供的模型 ID
PaddlePaddle/PaddleOCR-VL-1.6
不要混在一起。
另外这一块跟 PaddleX / PaddleOCR 版本关系比较大,后面如果升级版本,建议重新确认当前版本对 genai_config / client_kwargs 的解析方式。
10. 我最后把 XCCL Patch 做进了自己的镜像
调试阶段我直接:
sql
docker commit <CONTAINER_ID> paddleocr-vllm-a770:working
这样最省事。
不过如果正式部署,还是 Dockerfile 更靠谱,不然过段时间自己都不知道这个镜像改过什么。
Dockerfile:
python
FROM vllm/vllm-openai-xpu:v0.27.1
USER root
RUN python - <<'PY'
path = "/opt/venv/lib/python3.12/site-packages/vllm/v1/worker/xpu_worker.py"
with open(path, "r") as f:
s = f.read()
old = """ if torch.distributed.is_xccl_available():
torch.distributed.all_reduce(torch.zeros(1).xpu())
"""
new = """ if (
torch.distributed.is_xccl_available()
and self.parallel_config.world_size > 1
):
torch.distributed.all_reduce(torch.zeros(1).xpu())
"""
if old not in s:
raise RuntimeError(
"Patch target not found. "
"vLLM source may have changed."
)
with open(path, "w") as f:
f.write(s.replace(old, new, 1))
PY
构建:
erlang
docker build -t paddleocr-vllm-a770:0.27.1 .
这个 Patch 是按:
bash
vllm/vllm-openai-xpu:v0.27.1
做的。
以后如果升级 vLLM,不要直接照搬,先看一下 xpu_worker.py 还一不一样。
11. 最终的 VLM Docker 启动方式
我现在让 VLM 固定跑第二张 A770:
ini
ZE_AFFINITY_MASK=1
命令:
ini
docker run -d \
--name paddleocr-vllm-a770 \
--user root \
--network paddleocr-a770-net \
--device /dev/dxg:/dev/dxg \
-v /usr/lib/wsl:/usr/lib/wsl:ro \
--cap-add SYS_PTRACE \
--security-opt seccomp=unconfined \
--ipc=host \
-e ZE_FLAT_DEVICE_HIERARCHY=COMPOSITE \
-e ZE_AFFINITY_MASK=1 \
-e VLLM_TRITON_USE_TD=0 \
-e HF_HUB_OFFLINE=1 \
-e TRANSFORMERS_OFFLINE=1 \
--entrypoint /bin/bash \
paddleocr-vllm-a770:0.27.1 \
-lc 'vllm serve PaddlePaddle/PaddleOCR-VL-1.6 \
--trust-remote-code \
--dtype half \
--max-model-len 8192 \
--max-num-batched-tokens 8192 \
--no-enable-prefix-caching \
--mm-processor-cache-gb 0 \
--gpu-memory-utilization 0.7 \
--attention-backend TRITON_ATTN \
--mm-encoder-attn-backend TORCH_SDPA \
--host 0.0.0.0 \
--port 8000'
这里我最终没有:
css
-p 8000:8000
因为 8000 只是底层 vLLM,PaddleOCR Pipeline 和它在同一个 Docker 网络里就可以直接访问。
如果只是调试,想从 WSL 宿主机 curl,可以临时加:
css
-p 127.0.0.1:8000:8000
比直接:
css
-p 8000:8000
更合适。
12. 再接 PaddleOCR Pipeline
前面的 vLLM API 不是我最终想给业务调用的接口。
业务真正需要的是:
PaddleOCR / PaddleX Pipeline API
里面还会负责:
Layout Detection
文档预处理
VLM Recognition
结果组织
所以最终还是两层:
业务请求
↓
PaddleOCR Pipeline
↓
vLLM
↓
PaddleOCR-VL
我先建一个 Docker 网络:
lua
docker network create paddleocr-a770-net
然后 paddleocr-vllm-a770 和 paddleocr-vl-api-a770 都放到这个网络。
Pipeline 访问:
bash
http://paddleocr-vllm-a770:8000/v1
13. 双 A770 我最后选择分开跑
一开始我也考虑过:
一张 A770
├── Pipeline
└── vLLM
理论上 16GB 对 0.9B 模型不是完全不够。
但 vLLM 本身会留比较大的 KV Cache,而 Pipeline 还要加载:
PP-DocLayoutV3
再加图片 tensor 和临时显存,放一张卡上没有必要。
既然机器上有两张 A770,我最后直接拆了:
bash
A770 #0
└── PaddleOCR Pipeline
A770 #1
└── PaddleOCR-VL / vLLM
Pipeline 用:
ini
ZE_AFFINITY_MASK=0
VLM 用:
ini
ZE_AFFINITY_MASK=1
最后结构大概是:
bash
Java / Python / Web
│
▼
PaddleOCR API
:18100
│
▼
Pipeline Container
A770 #0
│
│ Docker Network
▼
paddleocr-vllm-a770:8000
│
▼
vLLM 0.27.1
A770 #1
│
┌────────────┴────────────┐
│ │
TRITON_ATTN TORCH_SDPA
Decoder Vision Encoder
这种方式比较干净。
后面如果要压并发,也方便判断瓶颈到底是在:
Layout
还是:
VLM
14. ZE_AFFINITY_MASK=1 为什么容器里还是 xpu:0?
这个地方第一次看也容易误会。
例如我指定:
ini
ZE_AFFINITY_MASK=1
表示使用第二张物理 A770。
但进容器以后:
scss
torch.xpu.get_device_name(0)
还是:
makefile
xpu:0
这是正常的。
因为:
ini
物理 GPU #1
↓
ZE_AFFINITY_MASK=1
↓
其他 GPU 被隐藏
↓
容器现在只看到 1 张 GPU
↓
它自然重新编号成 xpu:0
所以不要因为容器里面显示 xpu:0 就以为它跑到了第一张物理卡。
15. 国内网络环境最好直接做离线缓存
PaddleOCR-VL 模型比较大,第一次下载正常。
但我后面发现即使模型已经在缓存里,vLLM 启动时还是可能去 Hugging Face 检查。
网络不好的时候会看到:
csharp
Network is unreachable
或者:
makefile
SSL: UNEXPECTED_EOF_WHILE_READING
然后启动平白多等几分钟。
模型确认下载完整以后,我直接加:
ini
HF_HUB_OFFLINE=1
TRANSFORMERS_OFFLINE=1
Docker:
ini
-e HF_HUB_OFFLINE=1 \
-e TRANSFORMERS_OFFLINE=1
如果模型不是直接 commit 在镜像里,最好把缓存也挂到宿主机:
bash
-v /host/model/cache:/root/.cache/huggingface
这样容器删掉也不用重新下载。
16. 几个我实际用得比较多的排查命令
看 vLLM 日志:
bash
docker logs -f --tail 100 paddleocr-vllm-a770
看 Pipeline:
bash
docker logs -f --tail 200 paddleocr-vl-api-a770
看 EngineCore 有没有真死:
perl
docker exec paddleocr-vllm-a770 \
ps -eo pid,stat,%cpu,%mem,etime,cmd | grep -E 'vllm|EngineCore'
看容器资源:
css
docker stats --no-stream paddleocr-vllm-a770
看 Docker 网络:
docker network inspect paddleocr-a770-net
如果 vLLM 映射到了宿主机:
arduino
curl http://127.0.0.1:8000/health
模型列表:
bash
curl http://127.0.0.1:8000/v1/models
17. 最后把这次几个坑放一起
如果以后我自己再部署一次,真正需要记住的其实就这些。
官方镜像启动成 Qwen
先查:
objectivec
ENTRYPOINT / CMD
不要先怀疑 PaddleOCR 模型本身。
IPEX 报
sql
IPEX SDP only supports half datatype
先:
css
--dtype half
报
Unsupported gpu_arch of paged_attention_vllm!!
我这里继续折腾旧 IPEX 没意义,最后直接换了新版 vLLM XPU。
新版 vLLM 在 WSL2 Segfault
如果堆栈在:
arduino
ProcessGroupXCCL::allreduce
单卡可以检查是不是那个无意义的 XCCL warmup。
报
csharp
Only XE2/XE3 cutlass kernel is supported currently.
A770 不走这个 XPU FlashAttention kernel。
最终:
Decoder → TRITON_ATTN
Vision → TORCH_SDPA
Pipeline 调 vLLM 404
先查:
bash
curl http://127.0.0.1:8000/v1/models
看看 vLLM 真实提供的模型 ID。
不要只看 PaddleOCR 配置里的模型名字。
18. 这次部署下来最大的感受
一开始看到"Intel GPU 镜像"的时候,我其实默认理解成:
A770 应该开箱就能跑。
实际并不是。
Intel GPU 本身也有明显的架构代际,而 AI 推理框架里真正决定能不能跑的,很多时候不是:
python
torch.xpu.is_available() == True
而是某一个非常具体的 kernel 到底有没有覆盖你的 GPU 架构。
这次就是一个很典型的例子。
A770 从头到尾:
bash
驱动正常
/dev/dxg 正常
PyTorch XPU 正常
模型也能加载
但还是连续撞上:
objectivec
IPEX SDP
Paged Attention
XCCL
FlashAttention
这些更底层的问题。
最后真正解决它的办法也不是"换一个 OCR 模型",而是把推理链重新组合了一遍:
diff
PyTorch XPU
+
vLLM 0.27.1
+
单卡 XCCL Patch
+
TRITON_ATTN
+
TORCH_SDPA
到这里 PaddleOCR-VL-1.6 才算真正跑起来。
但是现在稳定性和吞吐没有进行测试,如果要考虑生产部署的话,后续还需要进行相关测试。