PaddleOCR-VL 华为昇腾 910A NPU 适配详细报告

项目 : 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

关键发现:

  1. 图像太小(<600)反而更慢,因为识别质量下降导致产生更多"幻觉"文本
  2. 800-1000 是最佳尺寸范围
  3. 图像需对齐到 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

问题:

  1. 总输出字数 2537 > 单图 2216,产生重复内容
  2. 模型重复加载开销大
  3. 各块耗时不均衡

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

相关推荐
我不是程序猿儿3 小时前
【C#】软件设计,华为的IPD学习之需求开发心得
学习·华为·c#
音浪豆豆_Rachel4 小时前
Flutter鸿蒙文件选择器进阶解析:多图选择的实现
flutter·华为·harmonyos
m0_6855350812 小时前
监控广角镜头架构选择
华为·光学·光学设计·光学工程·镜头设计
柒儿吖13 小时前
纯脚本项目的跨平台适配方法论:getoptions在开源鸿蒙PC平台的实现解析
华为·开源·harmonyos
搬砖的kk14 小时前
基于Flutter开发应用如何快速适配HarmonyOS
flutter·华为·harmonyos
音浪豆豆_Rachel17 小时前
Flutter跨平台通信的智能配置:Pigeon注解配置与鸿蒙生态深度集成
flutter·华为·harmonyos
Yeats_Liao20 小时前
MindSpore开发之路(八):数据处理之Dataset(上)——构建高效的数据流水线
数据结构·人工智能·python·机器学习·华为
FrameNotWork20 小时前
HarmonyOS 教学实战(六):复杂表单与校验体系(把“最难写”的模块写优雅)
华为·harmonyos
HMS Core21 小时前
【FAQ】HarmonyOS SDK 闭源开放能力 — Form Kit
华为·harmonyos