ITADN

RFC: Ascend NPU (华为昇腾) 设备适配支持

#1549Openpjgao 创建于 2026-05-17
P
pjgaocommented
# RFC: VLMEvalKit 华为昇腾 NPU 适配方案 | 字段 | 内容 | |------|------| | **标题** | VLMEvalKit NPU (Ascend) 设备适配支持 | | **作者** | gaopengju | | **日期** | 2026-05-17 | | **状态** | Draft | | **目标仓库** | open-compass/VLMEvalKit | --- ## 1. 背景与动机 ### 1.1 现状 VLMEvalKit 是目前最活跃的开源多模态大模型评测框架之一,支持 220+ LMMs 和 80+ benchmarks。然而,整个代码仓**深度绑定 NVIDIA CUDA 生态**,在非 CUDA 加速器(如华为昇腾 NPU)上完全无法运行。 ### 1.2 问题规模 通过对代码仓的全面审计,我们发现了以下 CUDA 硬编码依赖: | 类别 | 数量 | 影响级别 | |------|------|---------| | `torch.cuda.empty_cache()` 无条件调用 | ~40 处 | 高 - 非CUDA环境直接报错 | | `.cuda()` / `.to('cuda')` 硬编码 | ~60+ 处 | 高 - 模型/张量无法迁移到NPU | | `device_map='cuda'` 硬编码 | ~20 处 | 高 - 模型加载失败 | | `self.device = 'cuda'` 硬编码 | ~10 处 | 高 - 推理时设备错误 | | `flash_attention_2` 硬编码 | ~17 处 | 高 - NPU不支持flash_attention_2 | | `torch.cuda.device_count()` | ~15 处 | 中 - GPU计数错误 | | `torch.cuda.set_device()` | ~6 处 | 中 - 设备设置失败 | | `torch.cuda.synchronize()` | ~2 处 | 中 - 同步失败 | | `torch.cuda.amp.autocast()` | ~5 处 | 低 - 可用torch.amp替代 | | `nvidia-smi` 命令调用 | ~2 处 | 中 - 命令不存在 | | `nccl` backend 硬编码 | ~1 处 | 中 - NPU使用hccl | | `torch.cuda.manual_seed_all()` | ~6 处 | 低 - 种子设置失败 | | vLLM 硬编码 | ~10 处 | 低 - 依赖vLLM自身NPU支持 | ### 1.3 目标 使 VLMEvalKit 能够在华为昇腾 NPU(Ascend 910B2 等)上直接运行,评测本地多模态模型,包括: - 支持 `torch_npu` 后端 - 支持 HCCL 分布式后端 - 支持 NPU 设备检测和自动分配 - 保持与现有 CUDA 代码的完全兼容 --- ## 2. 技术方案 ### 2.1 核心设计原则 1. **零侵入兼容**:所有修改必须保持现有 CUDA 功能不受影响 2. **统一设备抽象**:引入设备管理工具模块,所有设备相关操作通过统一接口 3. **渐进式适配**:分优先级修改,核心框架先行,模型适配后行 4. **环境自动检测**:根据运行环境自动选择 CUDA/NPU/CPU ### 2.2 新增设备管理模块 `vlmeval/utils/device.py` 这是整个适配方案的核心,提供统一的设备管理接口: ```python """Unified device management for VLMEvalKit. Supports CUDA, Ascend NPU, MPS, and CPU backends. """ import os import torch _BACKEND = None def _detect_backend(): """Detect the best available compute backend.""" global _BACKEND if _BACKEND is not None: return _BACKEND if torch.cuda.is_available(): _BACKEND = 'cuda' return 'cuda' try: import torch_npu # noqa: F401 if torch.npu.is_available(): _BACKEND = 'npu' return 'npu' except ImportError: pass if hasattr(torch.backends, 'mps') and torch.backends.mps.is_available(): _BACKEND = 'mps' return 'mps' _BACKEND = 'cpu' return 'cpu' def get_backend(): """Get the current compute backend name.""" return _detect_backend() def get_device(): """Get the best available torch device.""" backend = get_backend() return torch.device(backend) def get_device_name(): """Get device name string (e.g., 'cuda', 'npu', 'cpu').""" return get_backend() def get_device_count(): """Get the number of available accelerator devices.""" backend = get_backend() if backend == 'cuda': return torch.cuda.device_count() elif backend == 'npu': return torch.npu.device_count() return 0 def get_device_memory(device_index=0): """Get free memory of the specified device in MB.""" backend = get_backend() if backend == 'cuda': try: import subprocess command = f"nvidia-smi --query-gpu=memory.free --format=csv -i {device_index}" output = subprocess.check_output(command.split()).decode('ascii') return int(output.strip().split('\n')[1].split()[0]) except Exception: return 0 elif backend == 'npu': try: import subprocess command = f"npu-smi info -t usages -i {device_index}" output = subprocess.check_output(command.split()).decode('utf-8') for line in output.split('\n'): if 'HBM' in line or 'Memory' in line: parts = line.split() for i, p in enumerate(parts): if 'MB' in p.upper() or '/' in p: free_str = p.split('/')[-1].strip() if '/' in p else parts[i+1] return int(''.join(c for c in free_str if c.isdigit())) return 0 except Exception: return 0 return 0 def get_all_device_memory(): """Get free memory of all available devices in MB.""" count = get_device_count() return [get_device_memory(i) for i in range(count)] def set_device(device_index): """Set the current device.""" backend = get_backend() if backend == 'cuda': torch.cuda.set_device(device_index) elif backend == 'npu': torch.npu.set_device(device_index) def empty_cache(): """Safely empty the cache of the current device.""" backend = get_backend() if backend == 'cuda': torch.cuda.empty_cache() elif backend == 'npu': torch.npu.empty_cache() def synchronize(): """Safely synchronize the current device.""" backend = get_backend() if backend == 'cuda': torch.cuda.synchronize() elif backend == 'npu': torch.npu.synchronize() def manual_seed_all(seed): """Set random seed for all devices.""" backend = get_backend() if backend == 'cuda': torch.cuda.manual_seed_all(seed) elif backend == 'npu': torch.npu.manual_seed_all(seed) def get_distributed_backend(): """Get the appropriate distributed backend.""" backend = get_backend() if backend == 'cuda': return 'nccl' elif backend == 'npu': return 'hccl' return 'gloo' def get_attn_implementation(): """Get the best attention implementation for current device.""" backend = get_backend() if backend == 'cuda': return 'flash_attention_2' return 'eager' def get_visible_devices_env_var(): """Get the environment variable name for visible devices.""" backend = get_backend() if backend == 'cuda': return 'CUDA_VISIBLE_DEVICES' elif backend == 'npu': return 'ASCEND_RT_VISIBLE_DEVICES' return None def get_visible_devices(): """Get the list of visible device indices.""" env_var = get_visible_devices_env_var() if env_var is None: return [] devices_str = os.environ.get(env_var, '') if not devices_str: return list(range(get_device_count())) return [int(x) for x in devices_str.split(',')] def get_accelerator_list(): """Get the list of accelerator devices (replaces get_gpu_list in run.py).""" env_var = get_visible_devices_env_var() if env_var is None: return [] devices_str = os.environ.get(env_var, '') if devices_str: return [int(x) for x in devices_str.split(',')] # Fallback: try device-specific detection try: return list(range(get_device_count())) except Exception: return [] ``` ### 2.3 修改方案:按优先级分层 #### 第一层:核心框架修改(必须,影响所有模型) ##### 2.3.1 `run.py` - 入口文件 **问题1:`get_gpu_list()` 依赖 nvidia-smi** ```python # 原代码 (行19-30) def get_gpu_list(): CUDA_VISIBLE_DEVICES = os.environ.get('CUDA_VISIBLE_DEVICES', '') if CUDA_VISIBLE_DEVICES != '': gpu_list = [int(x) for x in CUDA_VISIBLE_DEVICES.split(',')] return gpu_list try: ps = subprocess.Popen(('nvidia-smi', '--list-gpus'), stdout=subprocess.PIPE) output = subprocess.check_output(('wc', '-l'), stdin=ps.stdout) return list(range(int(output))) except Exception: return [] ``` ```python # 修改后 from vlmeval.utils.device import get_accelerator_list, get_visible_devices_env_var, get_device_name def get_gpu_list(): env_var = get_visible_devices_env_var() devices_str = os.environ.get(env_var, '') if env_var else '' if devices_str: return [int(x) for x in devices_str.split(',')] try: ps = subprocess.Popen(('nvidia-smi', '--list-gpus'), stdout=subprocess.PIPE) output = subprocess.check_output(('wc', '-l'), stdin=ps.stdout) return list(range(int(output))) except Exception: try: import torch_npu # noqa: F401 return list(range(torch.npu.device_count())) except Exception: return [] ``` **问题2:`CUDA_VISIBLE_DEVICES` 硬编码(行38-47)** ```python # 原代码 GPU_LIST = get_gpu_list() if LOCAL_WORLD_SIZE > 1 and len(GPU_LIST): NGPU = len(GPU_LIST) ... CUDA_VISIBLE_DEVICES = [str(i) for i in GPU_LIST[DEVICE_START_IDX: DEVICE_START_IDX + GPU_PER_PROC]] CUDA_VISIBLE_DEVICES = ','.join(CUDA_VISIBLE_DEVICES) os.environ['CUDA_VISIBLE_DEVICES'] = CUDA_VISIBLE_DEVICES ``` ```python # 修改后 from vlmeval.utils.device import get_visible_devices_env_var GPU_LIST = get_gpu_list() if LOCAL_WORLD_SIZE > 1 and len(GPU_LIST): NGPU = len(GPU_LIST) assert NGPU >= LOCAL_WORLD_SIZE GPU_PER_PROC = NGPU // LOCAL_WORLD_SIZE DEVICE_START_IDX = GPU_PER_PROC * LOCAL_RANK VISIBLE_DEVICES = [str(i) for i in GPU_LIST[DEVICE_START_IDX: DEVICE_START_IDX + GPU_PER_PROC]] VISIBLE_DEVICES_STR = ','.join(VISIBLE_DEVICES) env_var = get_visible_devices_env_var() if env_var: os.environ[env_var] = VISIBLE_DEVICES_STR print( f'RANK: {RANK}, LOCAL_RANK: {LOCAL_RANK}, WORLD_SIZE: {WORLD_SIZE},' f'LOCAL_WORLD_SIZE: {LOCAL_WORLD_SIZE}, {env_var}: {VISIBLE_DEVICES_STR}' ) ``` **问题3:`nccl` backend 硬编码(行527)** ```python # 原代码 dist.init_process_group( backend='nccl', timeout=datetime.timedelta(seconds=int(os.environ.get('DIST_TIMEOUT', 3600))) ) ``` ```python # 修改后 from vlmeval.utils.device import get_distributed_backend dist.init_process_group( backend=get_distributed_backend(), timeout=datetime.timedelta(seconds=int(os.environ.get('DIST_TIMEOUT', 3600))) ) ``` ##### 2.3.2 `vlmeval/inference.py` - 推理核心 **问题:`torch.cuda.synchronize()` 和 `torch.cuda.empty_cache()`** ```python # 原代码 (行185, 190) torch.cuda.synchronize() ... torch.cuda.empty_cache() ``` ```python # 修改后 from vlmeval.utils.device import synchronize, empty_cache synchronize() ... empty_cache() ``` ##### 2.3.3 `vlmeval/inference_mt.py` - 多轮推理 ```python # 原代码 (行167) torch.cuda.empty_cache() # 修改后 from vlmeval.utils.device import empty_cache empty_cache() ``` ##### 2.3.4 `vlmeval/inference_video.py` - 视频推理 ```python # 原代码 (行210, 215) torch.cuda.synchronize() torch.cuda.empty_cache() # 修改后 from vlmeval.utils.device import synchronize, empty_cache synchronize() empty_cache() ``` ##### 2.3.5 `vlmeval/smp/misc.py` - GPU 内存检测 ```python # 原代码 (行277-287) def get_gpu_memory(): import subprocess try: command = "nvidia-smi --query-gpu=memory.free --format=csv" memory_free_info = subprocess.check_output(command.split()).decode('ascii').split('\n')[:-1][1:] memory_free_values = [int(x.split()[0]) for i, x in enumerate(memory_free_info)] return memory_free_values except Exception as e: print(f'{type(e)}: {str(e)}') return [] ``` ```python # 修改后 def get_gpu_memory(): from vlmeval.utils.device import get_backend, get_all_device_memory backend = get_backend() if backend == 'cuda': try: import subprocess command = "nvidia-smi --query-gpu=memory.free --format=csv" memory_free_info = subprocess.check_output(command.split()).decode('ascii').split('\n')[:-1][1:] memory_free_values = [int(x.split()[0]) for i, x in enumerate(memory_free_info)] return memory_free_values except Exception as e: print(f'{type(e)}: {str(e)}') return [] elif backend == 'npu': return get_all_device_memory() return [] ``` ##### 2.3.6 `vlmeval/tools.py` - GPU 计数 ```python # 原代码 (行387) NGPU = torch.cuda.device_count() # 修改后 from vlmeval.utils.device import get_device_count NGPU = get_device_count() ``` #### 第二层:VLM 模型适配(重点模型优先) ##### 2.3.7 通用替换模式 所有 VLM 模型实现中的 CUDA 硬编码,按以下模式替换: | 原代码 | 替换为 | |--------|--------| | `self.device = 'cuda'` | `from vlmeval.utils.device import get_device_name; self.device = get_device_name()` | | `.cuda()` | `.to(self.device)` | | `.to('cuda')` | `.to(self.device)` 或 `.to(self.model.device)` | | `torch.cuda.device_count()` | `from vlmeval.utils.device import get_device_count; get_device_count()` | | `torch.cuda.set_device(0)` | `from vlmeval.utils.device import set_device; set_device(0)` | | `torch.cuda.empty_cache()` | `from vlmeval.utils.device import empty_cache; empty_cache()` | | `torch.cuda.manual_seed_all(0)` | `from vlmeval.utils.device import manual_seed_all; manual_seed_all(0)` | | `device_map='cuda'` | `device_map=get_device_name()` 或 `device_map='auto'` | | `attn_implementation='flash_attention_2'` | `from vlmeval.utils.device import get_attn_implementation; attn_implementation=get_attn_implementation()` | | `torch.cuda.amp.autocast()` | `torch.amp.autocast(device_type=get_device_name())` | | `torch.cuda.is_available()` | 需扩展检测逻辑,见 device.py 中的 `_detect_backend()` | ##### 2.3.8 重点模型修改示例 **Qwen2-VL (`vlmeval/vlm/qwen2_vl/model.py`)** ```python # 修改1: 添加导入 from vlmeval.utils.device import ( get_device_name, get_device_count, set_device, empty_cache, get_attn_implementation, get_all_device_memory ) # 修改2: GPU内存检查 (行265-267) # 原代码: # gpu_mems = get_gpu_memory() # max_gpu_mem = max(gpu_mems) if gpu_mems != [] else -1 # assert max_gpu_mem > 0 # 修改后: gpu_mems = get_all_device_memory() max_gpu_mem = max(gpu_mems) if gpu_mems else -1 if max_gpu_mem <= 0: import logging logging.warning(f'No accelerator memory detected, falling back to CPU. mems={gpu_mems}') # 修改3: vLLM路径 (行275) # 原代码: gpu_count = torch.cuda.device_count() # 修改后: gpu_count = get_device_count() # 修改4: LMDeploy路径 (行303-309) # 原代码: # num_gpus = torch.cuda.device_count() # torch.cuda.set_device(0) # self.device = 'cuda' # 修改后: num_gpus = get_device_count() set_device(0) self.device = get_device_name() # 修改5: Transformers路径 (行312) # 原代码: device_map="auto", attn_implementation='flash_attention_2' # 修改后: device_map="auto", attn_implementation=get_attn_implementation() # 修改6: 推理 (行487) # 原代码: inputs = inputs.to('cuda') # 修改后: inputs = inputs.to(self.model.device) ``` **InternVL (`vlmeval/vlm/internvl/internvl_chat.py`)** ```python # 修改1: 添加导入 from vlmeval.utils.device import get_device_name, get_device_count, set_device, empty_cache # 修改2: LMDeploy路径 (行168-179) # 原代码: # num_gpus = torch.cuda.device_count() # torch.cuda.set_device(0) # self.device = 'cuda' # 修改后: num_gpus = get_device_count() set_device(0) self.device = get_device_name() # 修改3: Transformers路径 (行187) # 原代码: self.device = 'cuda' # 修改后: self.device = get_device_name() ``` **MiniCPM-V (`vlmeval/vlm/minicpm_v.py`)** ```python # 修改1: 添加导入 from vlmeval.utils.device import ( get_device_name, set_device, empty_cache, manual_seed_all, get_device_count ) # 修改2: 所有模型类中的 .eval().cuda() -> .eval().to(get_device_name()) # 例如 (行30): # self.model = self.model.to(dtype=torch.bfloat16) # self.model.eval().cuda() # torch.cuda.empty_cache() # 修改后: self.model = self.model.to(dtype=torch.bfloat16) self.device = get_device_name() self.model.eval().to(self.device) empty_cache() # 修改3: manual_seed_all (行272, 551, 807, 1286) # 原代码: torch.cuda.manual_seed_all(0) # 修改后: manual_seed_all(0) ``` **LLaVA (`vlmeval/vlm/llava/llava.py`)** ```python # 修改1: 所有 .cuda() 调用改为 .to(self.device) # 修改2: 所有 .to("cuda", ...) 改为 .to(self.device, ...) # 修改3: self.device 初始化使用 get_device_name() ``` ##### 2.3.9 全量模型修改清单 以下文件需要按照上述通用替换模式进行修改: | 文件 | 硬编码数量 | 优先级 | |------|-----------|--------| | `vlmeval/vlm/qwen2_vl/model.py` | 9 | P0 | | `vlmeval/vlm/qwen3_vl/model.py` | 5 | P0 | | `vlmeval/vlm/internvl/internvl_chat.py` | 6 | P0 | | `vlmeval/vlm/llava/llava.py` | 15+ | P0 | | `vlmeval/vlm/minicpm_v.py` | 18 | P0 | | `vlmeval/vlm/llama4.py` | 5 | P1 | | `vlmeval/vlm/deepseek_vl2.py` | 2 | P1 | | `vlmeval/vlm/deepseek_vl.py` | 2 | P1 | | `vlmeval/vlm/gemma.py` | 8 | P1 | | `vlmeval/vlm/cogvlm.py` | 8 | P1 | | `vlmeval/vlm/phi3_vision.py` | 5 | P1 | | `vlmeval/vlm/nvlm.py` | 4 | P1 | | `vlmeval/vlm/yi_vl.py` | 3 | P1 | | `vlmeval/vlm/vila.py` | 2 | P1 | | `vlmeval/vlm/wemm.py` | 2 | P2 | | `vlmeval/vlm/wethink_vl.py` | 4 | P2 | | `vlmeval/vlm/vlm_r1.py` | 4 | P2 | | `vlmeval/vlm/vlaa_thinker.py` | 4 | P2 | | `vlmeval/vlm/treevgr.py` | 4 | P2 | | `vlmeval/vlm/thyme/model.py` | 4 | P2 | | `vlmeval/vlm/smolvlm.py` | 4 | P2 | | `vlmeval/vlm/ristretto.py` | 3 | P2 | | `vlmeval/vlm/points.py` | 2 | P2 | | `vlmeval/vlm/phi4_multimodal.py` | 3 | P2 | | `vlmeval/vlm/oryx.py` | 5 | P2 | | `vlmeval/vlm/omchat.py` | 3 | P2 | | `vlmeval/vlm/monkey.py` | 6 | P2 | | `vlmeval/vlm/moondream.py` | 6 | P2 | | `vlmeval/vlm/mmalaya.py` | 5 | P2 | | `vlmeval/vlm/minimonkey.py` | 4 | P2 | | `vlmeval/vlm/mantis.py` | 3 | P2 | | `vlmeval/vlm/mixsense.py` | 1 | P2 | | `vlmeval/vlm/molmo.py` | 1 | P2 | | `vlmeval/vlm/long_vita.py` | 4 | P2 | | `vlmeval/vlm/kimi_vl.py` | 1 | P2 | | `vlmeval/vlm/keye_vlm/model.py` | 3 | P2 | | `vlmeval/vlm/h2ovl_mississippi.py` | 1 | P2 | | `vlmeval/vlm/vita.py` | 10+ | P3 | | `vlmeval/vlm/vlm3r.py` | 6 | P3 | | `vlmeval/vlm/valley/valley2.py` | 2 | P3 | | `vlmeval/vlm/valley/valley3.py` | 2 | P3 | | `vlmeval/vlm/video_llm/*.py` | 15+ | P3 | | `vlmeval/vlm/xcomposer/*.py` | 10+ | P3 | | `vlmeval/vlm/pandagpt.py` | 2 | P3 | | `vlmeval/vlm/parrot.py` | 3 | P3 | | `vlmeval/vlm/pixtral.py` | 1 | P3 | | `vlmeval/vlm/ross.py` | 3 | P3 | | `vlmeval/vlm/slime.py` | 3 | P3 | | `vlmeval/vlm/visualglm.py` | 1 | P3 | | `vlmeval/vlm/transcore_m.py` | 3 | P3 | | `vlmeval/vlm/open_flamingo.py` | 3 | P3 | | `vlmeval/vlm/omnilmm.py` | 2 | P3 | | `vlmeval/vlm/mplug_owl2.py` | 1 | P3 | | `vlmeval/vlm/mplug_owl3.py` | 2 | P3 | | `vlmeval/vlm/idefics.py` | 2 | P3 | | `vlmeval/vlm/eagle_x.py` | 1 | P3 | | `vlmeval/vlm/emu.py` | 4 | P3 | | `vlmeval/vlm/bagel_umm.py` | 3 | P3 | | `vlmeval/vlm/aria.py` | 1 | P3 | | `vlmeval/vlm/vxverse.py` | 1 | P3 | | `vlmeval/vlm/vintern_chat.py` | 1 | P3 | | `vlmeval/vlm/minigpt4.py` | 1 | P3 | | `vlmeval/vlm/kosmos.py` | 1 | P3 | | `vlmeval/vlm/janus.py` | 1 | P3 | | `vlmeval/vlm/instructblip.py` | 1 | P3 | | `vlmeval/vlm/deepseek_ocr.py` | 3 | P3 | | `vlmeval/vlm/covt.py` | 5 | P3 | | `vlmeval/vlm/cambrian_s.py` | 1 | P3 | | `vlmeval/vlm/falcon_vlm.py` | 1 | P3 | | `vlmeval/vlm/xgen_mm.py` | 2 | P3 | | `vlmeval/vlm/x_vl.py` | 2 | P3 | | `vlmeval/vlm/varco_vision.py` | 3 | P3 | | `vlmeval/vlm/qianfan_vl.py` | 2 | P3 | | `vlmeval/vlm/nanovlm.py` | 3 | P3 | | `vlmeval/vlm/llama_vision.py` | 2 | P3 | | `vlmeval/vlm/logics.py` | 1 | P3 | | `vlmeval/vlm/spatial_mllm.py` | 3 | P3 | | `vlmeval/vlm/rbdash.py` | 2 | P3 | | `vlmeval/vlm/sail_vl.py` | 2 | P3 | | `vlmeval/vlm/ovis/ovis.py` | 5 | P3 | | `vlmeval/vlm/cosmos.py` | 2 | P3 | | `vlmeval/vlm/hawk_vl/model.py` | 2 | P3 | | `vlmeval/vlm/qtunevl/*.py` | 8+ | P3 | | `vlmeval/vlm/ola/*.py` | 6+ | P3 | | `vlmeval/vlm/ursa/*.py` | 3 | P3 | | `vlmeval/api/hf_chat_model.py` | 3 | P2 | #### 第三层:Dataset 评估工具适配 ##### 2.3.10 `vlmeval/dataset/utils/` 目录 | 文件 | 修改内容 | |------|---------| | `verifier.py` (行213) | `torch.cuda.device_count()` -> `get_device_count()` | | `uni_svg.py` (行37) | `"cuda" if torch.cuda.is_available() else "cpu"` -> `get_device_name()` | | `design2code/visual_score.py` (行28) | 同上 | | `chartcap.py` (行140) | 同上 | | `SArena/video/viclip/viclip_vision.py` (行452-453) | `torch.cuda.manual_seed()` -> `manual_seed_all()` | | `SArena/video/CLIP_video.py` (行98) | 默认参数 `device=torch.device("cuda")` -> `device=None`,函数内动态检测 | | `SArena/LPIPS.py` (行41) | 同 uni_svg.py 模式 | | `SArena/DINO_Score.py` (行15) | 同上 | | `SArena/FID.py` (行25) | 同上 | | `SArena/runtime.py` (行39-53) | 扩展设备检测逻辑 | | `SArena/token_length.py` (行13) | 同上 | | `SArena/CLIP_Score.py` (行19) | 同上 | --- ## 3. NPU 特有注意事项 ### 3.1 torch_npu 兼容性 | 功能 | CUDA API | NPU 对应 API | 备注 | |------|----------|-------------|------| | 设备可用性 | `torch.cuda.is_available()` | `torch.npu.is_available()` | 需先 `import torch_npu` | | 设备计数 | `torch.cuda.device_count()` | `torch.npu.device_count()` | 同上 | | 设备设置 | `torch.cuda.set_device(idx)` | `torch.npu.set_device(idx)` | 同上 | | 缓存清理 | `torch.cuda.empty_cache()` | `torch.npu.empty_cache()` | 同上 | | 设备同步 | `torch.cuda.synchronize()` | `torch.npu.synchronize()` | 同上 | | 随机种子 | `torch.cuda.manual_seed_all()` | `torch.npu.manual_seed_all()` | 同上 | | 分布式后端 | `nccl` | `hccl` | torch.distributed 支持 | | 可见设备 | `CUDA_VISIBLE_DEVICES` | `ASCEND_RT_VISIBLE_DEVICES` | 环境变量 | | 设备管理命令 | `nvidia-smi` | `npu-smi` | CLI工具 | | 注意力实现 | `flash_attention_2` | `eager` | NPU暂不支持FA2 | | AMP | `torch.cuda.amp.autocast()` | `torch.npu.amp.autocast()` | 或 `torch.amp.autocast(device_type='npu')` | | 数据类型 | fp16/bf16 | bf16 优先 | NPU上bf16性能更优 | ### 3.2 transformers 兼容性 `device_map="auto"` 在 NPU 上的行为: - **PyTorch 2.1+ / transformers 4.36+**:`device_map="auto"` 已支持通过 `torch.npu` 识别 NPU 设备 - **建议**:优先使用 `device_map="auto"` 而非 `device_map="cuda"` 或 `device_map="npu"` - 对不支持 `device_map="auto"` 的模型,使用 `device_map=get_device_name()` ### 3.3 vLLM 兼容性 vLLM 目前对 NPU 的支持状态: - vLLM 社区正在推进 Ascend NPU 后端支持 - 在 NPU 适配完成前,vLLM 路径应自动禁用并回退到 Transformers 路径 - 修改方式:在 vLLM 导入时增加设备检测 ```python # 在使用 vLLM 的模型中 def _should_use_vllm(self): from vlmeval.utils.device import get_backend if get_backend() != 'cuda': return False try: from vllm import LLM # noqa: F401 return True except ImportError: return False ``` ### 3.4 LMDeploy 兼容性 LMDeploy 同样仅支持 CUDA,在 NPU 环境下应回退到 Transformers 路径。 ### 3.5 精度建议 | 硬件 | 推荐精度 | 备注 | |------|---------|------| | NVIDIA GPU | fp16/bf16 | flash_attention_2 可用 | | Ascend NPU | bf16 | bf16 计算性能优于 fp16 | | CPU | fp32 | 仅供测试 | 建议在 `device.py` 中添加精度建议函数: ```python def get_recommended_dtype(): """Get the recommended dtype for the current device.""" backend = get_backend() if backend in ('cuda', 'npu'): return torch.bfloat16 return torch.float32 ``` --- ## 4. 实施计划 ### Phase 1: 核心框架适配(1周) 1. 新增 `vlmeval/utils/device.py` 2. 修改 `run.py` - 设备检测和分布式后端 3. 修改 `vlmeval/inference.py` - 推理核心 4. 修改 `vlmeval/inference_mt.py` - 多轮推理 5. 修改 `vlmeval/inference_video.py` - 视频推理 6. 修改 `vlmeval/smp/misc.py` - GPU 内存检测 7. 修改 `vlmeval/tools.py` - GPU 计数 **验收标准**: - `python run.py --model Qwen2-VL-7B-Instruct --data MMBench_DEV_EN_V11` 在 NPU 上可以启动 - CUDA 环境下所有现有测试通过 ### Phase 2: P0 模型适配(1周) 1. `vlmeval/vlm/qwen2_vl/model.py` 2. `vlmeval/vlm/qwen3_vl/model.py` 3. `vlmeval/vlm/internvl/internvl_chat.py` 4. `vlmeval/vlm/llava/llava.py` 5. `vlmeval/vlm/minicpm_v.py` **验收标准**: - 以上5个模型在 NPU 上可以完成 MMBench_DEV_EN_V11 评测 - CUDA 环境下回归测试通过 ### Phase 3: P1 模型适配(1周) 1. llama4, deepseek_vl2, deepseek_vl, gemma, cogvlm, phi3_vision, nvlm, yi_vl, vila **验收标准**: - 所有 P1 模型在 NPU 上可以加载和推理 - CUDA 环境下回归测试通过 ### Phase 4: P2-P3 模型适配 + Dataset 适配(2周) 1. 剩余所有 VLM 模型 2. Dataset 评估工具中的 CUDA 硬编码 3. API 模型适配(hf_chat_model.py) **验收标准**: - 所有模型在 NPU 上可以加载 - 全量 benchmark 评测在 NPU 上通过 - CUDA 环境下回归测试通过 ### Phase 5: 文档和 CI(1周) 1. 添加 NPU 使用文档 2. 添加 NPU CI 流水线 3. 添加 `requirements_npu.txt` --- ## 5. 测试方案 ### 5.1 单元测试 新增 `tests/test_device.py`: ```python import pytest import torch def test_device_detection(): from vlmeval.utils.device import get_backend, get_device, get_device_count backend = get_backend() assert backend in ('cuda', 'npu', 'mps', 'cpu') device = get_device() assert isinstance(device, torch.device) def test_empty_cache_no_error(): from vlmeval.utils.device import empty_cache empty_cache() # 不应抛出异常 def test_synchronize_no_error(): from vlmeval.utils.device import synchronize synchronize() # 不应抛出异常 def test_distributed_backend(): from vlmeval.utils.device import get_distributed_backend backend = get_distributed_backend() assert backend in ('nccl', 'hccl', 'gloo') def test_attn_implementation(): from vlmeval.utils.device import get_attn_implementation impl = get_attn_implementation() assert impl in ('flash_attention_2', 'eager', 'sdpa') ``` ### 5.2 集成测试 | 测试场景 | 模型 | Benchmark | 硬件 | |---------|------|-----------|------| | NPU 单卡推理 | Qwen2-VL-7B-Instruct | MMBench_DEV_EN_V11 | Ascend 910B2 | | NPU 多卡推理 | InternVL2-26B | MMBench_DEV_EN_V11 | Ascend 910B2 x2 | | NPU 视频推理 | Qwen2-VL-7B-Instruct | MMBench_Video | Ascend 910B2 | | CUDA 回归测试 | Qwen2-VL-7B-Instruct | MMBench_DEV_EN_V11 | NVIDIA A100 | | CUDA 多卡回归 | InternVL2-26B | MMBench_DEV_EN_V11 | NVIDIA A100 x2 | ### 5.3 精度验证 NPU 上的评测精度应与 CUDA 上的结果在统计意义上等价(允许 ±0.5% 的精度差异,源于浮点计算差异)。 --- ## 6. 风险与缓解 | 风险 | 影响 | 缓解措施 | |------|------|---------| | `torch_npu` API 与 `torch.cuda` 不完全对齐 | 部分功能可能缺失 | 在 `device.py` 中添加 try/except 降级 | | transformers `device_map="auto"` 在 NPU 上行为异常 | 模型加载失败 | 提供手动 `device_map` 指定选项 | | 某些模型依赖 CUDA 特有算子 | 推理失败 | 记录不兼容模型,提供替代路径 | | vLLM/LMDeploy 不支持 NPU | 高吞吐推理不可用 | 自动回退到 Transformers 路径 | | NPU 上 bf16/fp16 精度差异 | 评测结果偏差 | 记录精度差异,提供校准方案 | | 大规模修改引入回归 bug | CUDA 环境功能受损 | 完善回归测试,CI 双硬件验证 | --- ## 7. 后续扩展 1. **支持更多加速器**:AMD ROCm (torch.hip)、Intel Gaudi (torch.hpu) 等 2. **NPU 性能优化**:适配 NPU 特有的算子优化和内存管理策略 3. **vLLM NPU 后端**:配合 vLLM 社区推进 NPU 支持 4. **自动混合精度**:根据设备类型自动选择最优精度策略 --- ## 8. 参考资料 - [torch_npu 官方文档](https://gitee.com/ascend/pytorch) - [华为昇腾 CANN 开发文档](https://www.hiascend.com/document) - [PyTorch 分布式后端 - HCCL](https://www.hiascend.com/document/detail/zh/CANNCommunityEdition/80RC3alpha00/DevelopmentGuide/torchdev/torchdev_0003.html) - [transformers device_map 文档](https://huggingface.co/docs/accelerate/usage_guides/big_modeling)
0 条评论