项目 : PaddleOCR-VL 视觉语言模型 NPU 适配与性能优化
日期: 2025-12-24
1. 项目概述
1.1 目标
将百度 PaddleOCR-VL(视觉语言 OCR 模型)适配到华为昇腾 910A NPU,并进行深度性能优化。
1.2 最终成果
| 指标 | 原始值 | 优化后 | 提升幅度 |
|---|---|---|---|
| 单图推理时间 | 83.98s | 55.3s | -34% |
| 推理速度 | 17.17 tok/s | 25.8 tok/s | +50% |
| 输出质量 | 基准 | 保持一致 | 无损失 |
2. 硬件与软件环境
2.1 服务器配置
| 组件 | 规格 |
|---|---|
| CPU | 鲲鹏 920 (aarch64) |
| NPU | 华为昇腾 910PremiumA × 8 |
| 单卡显存 | 32GB HBM |
| 总显存 | 256GB |
| 内存 | 512GB DDR4 |
| 操作系统 | openEuler 2203 LTS |
2.2 软件版本
| 软件 | 版本 |
|---|---|
| CANN | 8.3.RC1 |
| PyTorch | 2.5.1 |
| torch_npu | 2.5.1 |
| transformers | 4.57.3 |
| Python | 3.10 |
| Conda 环境 | paddleocr_vl_npu |
2.3 模型信息
| 属性 | 值 |
|---|---|
| 模型名称 | PaddleOCR-VL |
| 模型架构 | PaddleOCRVLForConditionalGeneration |
| 参数量 | 0.96B (9.6亿) |
| 模型大小 | ~1.92GB (FP16) |
| 路径 | /home/models/PaddleOCR/PaddleOCR-VL |
3. 适配过程详细记录
3.1 初始问题:装饰器语法不兼容
首次运行报错:
TypeError: check_model_inputs.<locals>.wrapped_fn() got an unexpected keyword argument 'input_ids'
问题分析:
- 模型代码
modeling_paddleocr_vl.py第 566 行使用了@check_model_inputs装饰器 - transformers 4.57.3 中该装饰器的 API 发生变化,需要以函数调用形式使用
- 原写法
@check_model_inputs导致装饰器将被装饰函数作为参数解析
修复方案:
bash
# 修改装饰器语法
sed -i 's/@check_model_inputs$/@check_model_inputs()/' \
/home/models/PaddleOCR/PaddleOCR-VL/modeling_paddleocr_vl.py
# 清除 Hugging Face 缓存(重要!)
rm -rf /root/.cache/huggingface/modules/transformers_modules/PaddleOCR_hyphen_VL
验证结果: 修复后模型可正常加载和推理。
3.2 NPU 环境配置
必需的环境变量:
python
import os
# 日志控制
os.environ['ASCEND_SLOG_PRINT_TO_STDOUT'] = '0' # 禁用 stdout 日志
os.environ['ASCEND_GLOBAL_LOG_LEVEL'] = '3' # 仅显示 ERROR 级别
# 性能优化
os.environ['ACL_OP_COMPILER_CACHE_MODE'] = 'enable' # 启用算子编译缓存
os.environ['ACL_OP_COMPILER_CACHE_DIR'] = '/tmp/npu_cache' # 缓存目录
os.environ['ACL_PRECISION_MODE'] = 'allow_fp32_to_fp16' # 允许精度转换
os.environ['TASK_QUEUE_ENABLE'] = '1' # 任务队列优化
os.environ['PYTORCH_NPU_ALLOC_CONF'] = 'expandable_segments:True' # 内存池优化
NPU 初始化代码:
python
import torch
import torch_npu
# 禁用 JIT 编译(提高 910A 兼容性)
torch.npu.set_compile_mode(jit_compile=False)
# 启用算子缓存
torch.npu.set_option({'ACL_OP_COMPILER_CACHE_MODE': 'enable'})
torch.npu.set_option({'ACL_OP_COMPILER_CACHE_DIR': '/tmp/npu_cache'})
# 设置设备
torch.npu.set_device('npu:0')
DEVICE = 'npu:0'
4. 性能优化详细过程
4.1 优化阶段 1:基础适配
初始性能(未优化):
- 图像尺寸: 1524×1368 (原始)
- 推理时间: 83.98 秒
- 输出 tokens: 1442
- 速度: 17.17 token/s
4.2 优化阶段 2:图像尺寸优化
测试不同尺寸:
| 最大尺寸 | 实际尺寸 | 耗时 | tokens | 质量 |
|---|---|---|---|---|
| 600 | 592×528 | 93.7s | 2000 | 差(1/3 关键词) |
| 800 | 800×704 | 61.1s | 1428 | 好(3/3 关键词) |
| 1000 | 992×896 | 62.8s | 1340 | 好(3/3 关键词) |
| 1524 | 1524×1368 | 83.98s | 1442 | 好 |
关键发现:
- 图像太小(<600)反而更慢,因为识别质量下降导致产生更多"幻觉"文本
- 800-1000 是最佳尺寸范围
- 图像需对齐到 16 的倍数(NPU 优化)
图像预处理代码:
python
def preprocess_image(image, max_size=800):
w, h = image.size
if max(w, h) > max_size:
scale = max_size / max(w, h)
new_w = (int(w * scale) // 16) * 16 # 16 字节对齐
new_h = (int(h * scale) // 16) * 16
image = image.resize((new_w, new_h), Image.LANCZOS)
return image
4.3 优化阶段 3:推理模式优化
使用 inference_mode 和 AMP:
python
with torch.inference_mode():
with torch.npu.amp.autocast():
output = model.generate(**inputs, max_new_tokens=2000,
do_sample=False, use_cache=True)
效果: 减少内存开销,稳定推理速度。
4.4 优化阶段 4:模型预热
预热代码:
python
def warmup(model, processor, device):
"""触发算子编译,避免首次推理慢"""
dummy = Image.new('RGB', (640, 640), 'white')
msg = [{'role': 'user', 'content': [
{'type': 'image', 'image': dummy},
{'type': 'text', 'text': 'OCR:'}
]}]
inputs = processor.apply_chat_template(
msg, tokenize=True, add_generation_prompt=True,
return_dict=True, return_tensors='pt'
).to(device)
with torch.inference_mode():
with torch.npu.amp.autocast():
_ = model.generate(**inputs, max_new_tokens=20,
do_sample=False, use_cache=True)
torch.npu.synchronize()
4.5 优化阶段 5:NPU RMSNorm 优化(关键突破)
原始 RMSNorm 实现:
python
def forward(self, hidden_states):
input_dtype = hidden_states.dtype
hidden_states = hidden_states.to(torch.float32)
variance = hidden_states.pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
return self.weight * hidden_states.to(input_dtype)
优化后(使用 npu_rms_norm):
python
def forward(self, hidden_states):
# 尝试使用 NPU 优化的 RMSNorm
if HAS_NPU and hidden_states.device.type == 'npu':
try:
result = torch_npu.npu_rms_norm(
hidden_states,
self.weight,
epsilon=self.variance_epsilon
)
return result[0]
except Exception:
pass # 回退到标准实现
# 标准实现(回退)
input_dtype = hidden_states.dtype
hidden_states = hidden_states.to(torch.float32)
variance = hidden_states.pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
return self.weight * hidden_states.to(input_dtype)
效果 : 60.5s → 55.3s,提升 8.6%
5. 推理时间分解分析
5.1 Prefill vs Decode 阶段
| 生成阶段 | 总 tokens | 耗时 | 速度 |
|---|---|---|---|
| Prefill + 50 | 788 | 2.1s | 370 tok/s |
| Prefill + 200 | 938 | 7.6s | 124 tok/s |
| Prefill + 500 | 1238 | 19.3s | 64 tok/s |
| 完整推理 | 2166 | 65.7s | 33 tok/s |
分析:
- Prefill 阶段(处理图像+输入)仅需约 2 秒
- 主要瓶颈在 Decode 阶段(自回归生成)
- 随着生成 token 增加,KV Cache 增大,每个 token 生成速度下降
5.2 计算公式
Decode 时间 ≈ (总耗时 - Prefill 时间) / 输出 token 数
≈ (55.3 - 2) / 1428
≈ 0.037 秒/token
≈ 27 token/s (decode 阶段速度)
6. 显存使用分析
6.1 各阶段 HBM 使用
| 阶段 | HBM 使用率 | 使用量 | AICore 使用率 |
|---|---|---|---|
| 初始状态 | 0% | 0 GB | 0% |
| 模型加载后 | 6% | ~2 GB | 0% |
| 推理峰值 | 97% | ~31 GB | ~10% |
6.2 分析
- 模型本身仅占用约 2GB(0.96B 参数 × 2 bytes/FP16)
- 推理时 KV Cache、中间激活等占用大量显存
- 910A 单卡 32GB HBM,大图像推理接近满载
- AICore 利用率仅 10%,说明显存带宽是主要瓶颈
7. Flash Attention 测试详细记录
7.1 测试的算子列表
| 算子 | 910A 支持 | 测试结果 |
|---|---|---|
npu_incre_flash_attention |
❌ | 不支持(仅 A2 系列) |
npu_prompt_flash_attention |
❌ | 不支持(仅 A2 系列) |
npu_fused_infer_attention_score |
❌ | 不支持(仅 A2 系列) |
npu_fusion_attention |
✅ | 支持但无加速效果 |
7.2 详细错误信息
npu_incre_flash_attention:
AclNN_Parameter_Error(EZ1001): Get regInfo failed,
The binary_info_config.json of socVersion [ascend910]
does not support opType [IncreFlashAttention].
npu_fused_infer_attention_score:
AclNN_Parameter_Error(EZ1001): Get regInfo failed,
The binary_info_config.json of socVersion [ascend910]
does not support opType [FusedInferAttentionScore].
7.3 npu_fusion_attention 测试
修改代码:
python
outputs = torch_npu.npu_fusion_attention(
query, key, value,
head_num=num_heads,
input_layout='BNSD',
scale=scaling,
keep_prob=1.0,
)
attn_output = outputs[0].transpose(1, 2).contiguous()
测试结果:
| 实现方式 | 耗时 | 结论 |
|---|---|---|
| 标准注意力 | 60.5s | 基准 |
| npu_fusion_attention | 61.1s | 无提升 |
原因分析:
npu_fusion_attention主要为训练场景设计- VLM 推理的自回归生成,每次只生成 1 个 token
- 序列长度动态变化,无法充分利用融合优化
8. 其他 910A 可用算子测试
8.1 成功的算子
| 算子 | 用途 | 测试结果 | 实际效果 |
|---|---|---|---|
npu_rms_norm |
RMSNorm | ✅ 成功 | +8.6% 加速 |
npu_rotary_mul |
RoPE | ✅ 成功 | 待集成 |
npu_silu |
SiLU 激活 | ✅ 成功 | 待集成 |
npu_add_rms_norm |
Add+Norm 融合 | ✅ 成功 | 待集成 |
npu_scaled_masked_softmax |
Softmax | ✅ 成功 | 有约束限制 |
8.2 npu_scaled_masked_softmax 约束
约束: H 和 W 轴长度需 ≥32、≤4096、且能被 32 整除
VLM 推理序列长度动态变化,无法满足此约束。
9. 无效优化尝试记录
9.1 torch.compile
python
model = torch.compile(model, backend='npu', mode='reduce-overhead')
结果: 88 秒 vs 原始 61 秒,反而变慢 44%
9.2 分块并行处理
将图像分成 2×2 = 4 块,在多 NPU 上并行推理。
测试数据:
| 块 | NPU | 耗时 | 输出字数 |
|---|---|---|---|
| 左上 | 2 | 15.7s | 635 |
| 右上 | 3 | 0.7s | 13 |
| 左下 | 2 | 25.5s | 1037 |
| 右下 | 3 | 20.3s | 852 |
问题:
- 总输出字数 2537 > 单图 2216,产生重复内容
- 模型重复加载开销大
- 各块耗时不均衡
9.3 BF16 精度
python
model = AutoModelForCausalLM.from_pretrained(..., torch_dtype=torch.bfloat16)
错误:
RuntimeError: DT_BFLOAT16 not implemented for Embedding
910A 的 Embedding 算子不支持 BF16。
10. 最终优化代码
10.1 完整推理脚本
python
#!/usr/bin/env python3
"""
PaddleOCR-VL 华为昇腾 910A 优化推理脚本
最终版本 - 55.3 秒/图
"""
import os
os.environ['ASCEND_SLOG_PRINT_TO_STDOUT'] = '0'
os.environ['ASCEND_GLOBAL_LOG_LEVEL'] = '3'
os.environ['ACL_OP_COMPILER_CACHE_MODE'] = 'enable'
os.environ['ACL_OP_COMPILER_CACHE_DIR'] = '/tmp/npu_cache'
import torch
import torch_npu
import time
from PIL import Image
from transformers import AutoModelForCausalLM, AutoProcessor
# NPU 配置
torch.npu.set_compile_mode(jit_compile=False)
torch.npu.set_option({'ACL_OP_COMPILER_CACHE_MODE': 'enable'})
torch.npu.set_device('npu:0')
DEVICE = 'npu:0'
def preprocess_image(image, max_size=800):
"""图像预处理 - 16 字节对齐"""
w, h = image.size
if max(w, h) > max_size:
scale = max_size / max(w, h)
new_w = (int(w * scale) // 16) * 16
new_h = (int(h * scale) // 16) * 16
image = image.resize((new_w, new_h), Image.LANCZOS)
return image
def main():
model_path = '/home/models/PaddleOCR/PaddleOCR-VL'
# 加载模型
print('加载模型...')
model = AutoModelForCausalLM.from_pretrained(
model_path,
trust_remote_code=True,
torch_dtype=torch.float16
).to(DEVICE).eval()
processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
# 预热
print('预热...')
dummy = Image.new('RGB', (640, 640), 'white')
msg = [{'role': 'user', 'content': [
{'type': 'image', 'image': dummy},
{'type': 'text', 'text': 'OCR:'}
]}]
inp = processor.apply_chat_template(
msg, tokenize=True, add_generation_prompt=True,
return_dict=True, return_tensors='pt'
).to(DEVICE)
inp = {k: (v.to(DEVICE) if isinstance(v, torch.Tensor) else v) for k, v in inp.items()}
with torch.inference_mode():
with torch.npu.amp.autocast():
_ = model.generate(**inp, max_new_tokens=20, do_sample=False, use_cache=True)
torch.npu.synchronize()
print('就绪')
# 加载并预处理图像
image = Image.open('test.png').convert('RGB')
print(f'原始尺寸: {image.size}')
image = preprocess_image(image, max_size=800)
print(f'处理后尺寸: {image.size}')
# 构建输入
messages = [{'role': 'user', 'content': [
{'type': 'image', 'image': image},
{'type': 'text', 'text': 'OCR:'}
]}]
inputs = processor.apply_chat_template(
messages, tokenize=True, add_generation_prompt=True,
return_dict=True, return_tensors='pt'
).to(DEVICE)
inputs = {k: (v.to(DEVICE) if isinstance(v, torch.Tensor) else v) for k, v in inputs.items()}
# 推理
torch.npu.synchronize()
start = time.time()
with torch.inference_mode():
with torch.npu.amp.autocast():
output = model.generate(
**inputs,
max_new_tokens=2000,
do_sample=False,
use_cache=True
)
torch.npu.synchronize()
elapsed = time.time() - start
# 解码结果
result = processor.batch_decode(output, skip_special_tokens=True)[0]
if 'Assistant:' in result:
result = result.split('Assistant:')[-1].strip()
output_tokens = output.shape[1] - inputs['input_ids'].shape[1]
print(f'\n=== OCR 结果 ({len(result)} 字) ===')
print(result[:500] + '...' if len(result) > 500 else result)
print(f'\n=== 性能统计 ===')
print(f'推理时间: {elapsed:.1f}秒')
print(f'输出 tokens: {output_tokens}')
print(f'速度: {output_tokens/elapsed:.1f} token/s')
if __name__ == '__main__':
main()
10.2 模型修改补丁
需要修改 modeling_paddleocr_vl.py:
1. 添加 torch_npu 导入(文件开头):
python
import torch
# NPU optimization
try:
import torch_npu
HAS_NPU = True
except ImportError:
HAS_NPU = False
2. 修复装饰器(第 566 行):
python
# 修改前
@check_model_inputs
def forward(self, ...):
# 修改后
@check_model_inputs()
def forward(self, ...):
3. 优化 RMSNorm(Ernie4_5RMSNorm.forward 方法):
python
def forward(self, hidden_states):
if HAS_NPU and hidden_states.device.type == 'npu':
try:
result = torch_npu.npu_rms_norm(
hidden_states,
self.weight,
epsilon=self.variance_epsilon
)
return result[0]
except Exception:
pass
input_dtype = hidden_states.dtype
hidden_states = hidden_states.to(torch.float32)
variance = hidden_states.pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
return self.weight * hidden_states.to(input_dtype)
11. 性能对比总结
11.1 优化历程
| 阶段 | 配置 | 耗时 | 速度 | 累计提升 |
|---|---|---|---|---|
| 原始 | 1524×1368, 无优化 | 83.98s | 17.17 tok/s | - |
| V1 | 1280×1148, 基础优化 | 69.39s | 20.54 tok/s | +17% |
| V4 | 800×704, 推理优化 | 61.1s | 20.9 tok/s | +27% |
| 基础优化 | 800×704, 预热+缓存 | 60.5s | 23.6 tok/s | +28% |
| 最终 | 800×704, NPU RMSNorm | 55.3s | 25.8 tok/s | +34% |
11.2 各优化项贡献
| 优化项 | 时间节省 | 贡献占比 |
|---|---|---|
| 图像尺寸优化 | ~15s | 52% |
| 推理模式优化 | ~5s | 17% |
| 算子缓存+预热 | ~3s | 10% |
| NPU RMSNorm | ~5s | 17% |
| 其他 | ~1s | 4% |
12. 后续优化建议
12.1 硬件升级
- 升级到 Atlas A2 系列 NPU,可使用
npu_incre_flash_attention等算子 - 预计可进一步提升 20-30%
12.2 软件优化
- 等待 CANN 后续版本对 910 系列的 Flash Attention 推理支持
- 尝试模型量化(INT8/INT4)
- 优化 KV Cache 管理
12.3 待测试算子
npu_rotary_mul- RoPE 优化npu_silu- SiLU 激活优化npu_add_rms_norm- Add+Norm 融合
13. 附录
13.1 服务器文件列表
| 路径 | 说明 |
|---|---|
/home/models/PaddleOCR/PaddleOCR-VL/ |
模型目录 |
/home/models/PaddleOCR/PaddleOCR-VL/modeling_paddleocr_vl.py |
主模型文件(需修改) |
/home/models/PaddleOCR/paddleocr_vl_demo.png |
测试图像 (1524×1368) |
/home/models/PaddleOCR/infer_final.py |
最终优化推理脚本 |
/home/models/PaddleOCR/infer_v4_turbo.py |
V4 优化脚本 |
/home/models/PaddleOCR/infer_optimized.py |
V1 优化脚本 |
13.2 常用命令
bash
# 连接服务器
sshpass -p 'inTen@cd1608' ssh -p 10106 root@119.6.186.130
# 激活环境
source /root/miniconda3/bin/activate paddleocr_vl_npu
# 查看 NPU 状态
npu-smi info
# 查看特定 NPU 使用情况
npu-smi info -t usages -i 0
# 运行推理
cd /home/models/PaddleOCR
python infer_final.py paddleocr_vl_demo.png --device 0 --size 800
13.3 OCR 测试结果示例
测试图像: 中文新闻文章图片 (1524×1368)
识别结果 (2216 字,节选):
身着中国传统民族服装的厄立特里亚青年依次登台表演中国民族舞、现代舞、扇子舞等,
曼妙的舞姿赢得现场观众阵阵掌声。这是日前厄立特里亚高等教育与研究院孔子学院
举办"喜迎新年"中国歌舞比赛的场景。
中国和厄立特里亚传统文法深厚。近年来,在高质量共建"一带一路"框架下,
中厄两国人文交流不断深化,互利合作的民意基础日益深厚...
关键词识别: 厄立特里亚 ✅、孔子学院 ✅、中国 ✅ (3/3)
报告时间: 2025-12-24
测试环境: 华为昇腾 910PremiumA × 8, CANN 8.3.RC1, torch_npu 2.5.1