換到推理調(diào)優(yōu))
1. 從“atlas”這個詞說起它到底指什么第一次看到“atlas”這個項目標題很多人腦子里會同時冒出好幾個東西希臘神話里扛著地球的泰坦神、地理課本上的地圖冊、數(shù)據(jù)庫里的Atlas、還有華為昇騰生態(tài)里的Atlas系列硬件。這種一詞多義的情況在技術(shù)圈特別常見所以第一步不是急著動手而是先把語境鎖定。結(jié)合熱搜詞“atlas部署yolo”和“atlas 300v 24g 是運算加速卡嗎”來看這里說的atlas基本可以確定是華為昇騰Atlas系列的計算產(chǎn)品尤其是Atlas 300V這類推理卡。Atlas 300V 24G是一塊面向推理場景的AI加速卡基于昇騰310P處理器顯存24GB主要用來跑模型推理不是訓(xùn)練卡。它常被拿來和NVIDIA的T4、A10這類推理卡做對比在國產(chǎn)化替代、邊緣推理、視頻分析等場景里出現(xiàn)頻率很高。那“atlas部署yolo”這件事本質(zhì)上就是把YOLO系列目標檢測模型通過昇騰的CANN工具鏈轉(zhuǎn)換并部署到Atlas 300V這類加速卡上跑出推理結(jié)果。這件事聽起來簡單實際踩坑的人非常多因為昇騰生態(tài)和CUDA生態(tài)的差異不小模型轉(zhuǎn)換、算子支持、精度對齊、性能調(diào)優(yōu)每一步都有坑。這篇文章適合誰看如果你是剛拿到Atlas 300V卡、想把YOLO模型跑起來的算法工程師或者你正在做國產(chǎn)化推理方案選型想評估Atlas部署YOLO的可行性和工作量再或者你已經(jīng)在CUDA上跑通了YOLO現(xiàn)在要遷移到昇騰平臺——那這篇內(nèi)容就是給你寫的。我會從整體思路、環(huán)境準備、模型轉(zhuǎn)換、推理部署、性能調(diào)優(yōu)、問題排查幾個層面把整個流程拆開講清楚盡量讓你少走彎路。提示本文涉及的硬件和工具鏈以昇騰社區(qū)公開資料和常見實踐為基礎(chǔ)具體版本號、命令參數(shù)請以你實際拿到的CANN版本和官方文檔為準。不同版本的CANN在API和工具行為上可能有差異這一點務(wù)必注意。2. 整體思路拆解為什么Atlas部署YOLO不能照搬CUDA那套2.1 昇騰生態(tài)和CUDA生態(tài)的核心差異在CUDA上部署YOLO流程大概是PyTorch訓(xùn)練 → torch.onnx導(dǎo)出 → TensorRT轉(zhuǎn)換 → 推理。整個過程工具鏈成熟社區(qū)資料多遇到問題搜一下基本都有答案。但到了昇騰平臺流程變成了PyTorch訓(xùn)練 → ONNX導(dǎo)出 → ATC工具轉(zhuǎn)換OM模型 → AscendCL推理。中間多了一個ATC轉(zhuǎn)換環(huán)節(jié)而這個環(huán)節(jié)恰恰是最容易出問題的地方。ATC是Ascend Tensor Compiler的縮寫它負責把ONNX、Caffe、TensorFlow等格式的模型轉(zhuǎn)換成昇騰硬件能執(zhí)行的OM離線模型。你可以把它理解成昇騰版的TensorRT但成熟度和算子覆蓋度跟TensorRT還有差距。YOLO里的一些特殊算子比如Focus層、某些版本的SiLU激活、自定義的NMS后處理在ATC轉(zhuǎn)換時可能不被支持需要做算子替換或者用昇騰提供的自定義算子來實現(xiàn)。另一個差異是內(nèi)存管理。CUDA上用PyTorch或TensorRT顯存分配基本是自動的你不太需要關(guān)心。但昇騰的AscendCL需要你手動管理Device內(nèi)存包括輸入輸出buffer的申請、數(shù)據(jù)拷貝、釋放。雖然pyACLPython接口封裝了一部分但理解這套內(nèi)存模型對排查問題很有幫助。2.2 為什么選擇ONNX作為中間格式Y(jié)OLO模型從PyTorch到昇騰中間格式選ONNX是最穩(wěn)妥的。原因有幾個第一ONNX是開放標準PyTorch導(dǎo)出支持好ATC對ONNX的支持也相對成熟第二ONNX可以在導(dǎo)出后用onnxsim、onnxruntime等工具做圖優(yōu)化和驗證提前發(fā)現(xiàn)一些問題第三ONNX的算子集和昇騰支持的算子集有對應(yīng)關(guān)系轉(zhuǎn)換時映射關(guān)系比較清晰。當然也有直接用Caffe或TensorFlow的情況但YOLOv5/v8主流還是PyTorch訓(xùn)練所以O(shè)NNX是自然選擇。這里要注意ONNX的opset版本太新或太舊都可能導(dǎo)致ATC轉(zhuǎn)換失敗。根據(jù)經(jīng)驗opset 11到13是比較穩(wěn)的區(qū)間具體要看CANN版本。2.3 部署形態(tài)的選擇離線推理還是在線推理Atlas 300V部署YOLO有兩種常見形態(tài)一種是離線推理把視頻或圖片批量處理追求吞吐量另一種是在線推理接實時視頻流追求低延遲。兩種形態(tài)對模型轉(zhuǎn)換和推理代碼的要求不同。離線推理可以用更大的batch sizeATC轉(zhuǎn)換時把batch維度固定成較大值推理時一次處理多幀吞吐量高。在線推理則通常batch1ATC轉(zhuǎn)換時把batch維度設(shè)成動態(tài)或者固定為1延遲低但吞吐量小。選擇哪種形態(tài)取決于你的業(yè)務(wù)場景如果是視頻監(jiān)控事后分析離線更合適如果是實時告警在線更合適。注意Atlas 300V 24G的顯存是24GB聽起來很大但OM模型加載、輸入輸出buffer、中間特征圖都會占顯存。YOLOv5s的OM模型大概幾十MB但推理時的中間激活可能占幾個GB。如果batch size設(shè)得太大顯存可能不夠。建議先用小batch跑通再逐步加大。3. 環(huán)境準備從驅(qū)動到CANN的完整清單3.1 硬件和系統(tǒng)要求Atlas 300V是一塊PCIe加速卡需要插在服務(wù)器的PCIe插槽上。它對服務(wù)器有要求需要支持PCIe 4.0向下兼容3.0供電和散熱要滿足卡的TDP。具體TDP數(shù)值查一下官方規(guī)格書不同型號可能有差異。操作系統(tǒng)方面昇騰官方支持Ubuntu、CentOS、openEuler等具體版本看CANN文檔的兼容性列表。我建議用Ubuntu 20.04或openEuler 20.03 LTS這兩個系統(tǒng)在社區(qū)里資料最多遇到問題好搜。CentOS也可以用但要注意內(nèi)核版本和驅(qū)動兼容性。安裝系統(tǒng)時建議最小化安裝避免多余的服務(wù)和庫干擾。3.2 驅(qū)動和固件安裝Atlas 300V需要安裝NPU驅(qū)動和固件。驅(qū)動是讓操作系統(tǒng)識別到卡固件是卡內(nèi)部的運行環(huán)境。安裝順序一般是先固件后驅(qū)動或者按官方文檔的順序來。安裝包通常是一個.run文件執(zhí)行后按提示操作。安裝完成后用npu-smi info命令檢查卡是否被識別。正常輸出會顯示卡的型號、顯存使用情況、溫度、功耗等信息。如果這個命令報錯或者看不到卡說明驅(qū)動沒裝好需要檢查內(nèi)核模塊是否加載、設(shè)備節(jié)點是否存在。# 檢查NPU設(shè)備 npu-smi info # 查看驅(qū)動版本 cat /usr/local/Ascend/driver/version.info3.3 CANN工具鏈安裝CANN是昇騰計算語言的核心工具包包含ATC轉(zhuǎn)換工具、AscendCL運行時、算子庫等。安裝方式有.run安裝和rpm/deb安裝我習慣用.run安裝因為可以自定義安裝路徑卸載也方便。安裝CANN時要注意幾個點第一安裝用戶最好是普通用戶不要用root避免權(quán)限問題第二安裝路徑不要有中文和空格第三安裝完成后要source環(huán)境變量腳本把CANN的庫路徑加到LD_LIBRARY_PATH里。# 安裝CANN以某版本為例 ./Ascend-cann-toolkit_xxx_linux-x86_64.run --install # source環(huán)境變量 source /usr/local/Ascend/ascend-toolkit/set_env.sh安裝完成后用atc --version檢查ATC工具是否可用。如果提示找不到命令說明環(huán)境變量沒生效檢查set_env.sh是否source成功。3.4 Python環(huán)境和依賴昇騰提供了pyACL是AscendCL的Python封裝。另外還需要安裝numpy、opencv-python、onnx、onnxruntime等庫。建議用conda創(chuàng)建一個獨立環(huán)境避免和系統(tǒng)Python沖突。conda create -n atlas_yolo python3.8 conda activate atlas_yolo pip install numpy opencv-python onnx onnxruntimepyACL的安裝方式取決于CANN版本有些版本自帶pyACL的whl包在CANN安裝目錄下可以找到。安裝后import acl測試一下如果報錯找不到庫檢查LD_LIBRARY_PATH是否包含CANN的lib64目錄。提示Python版本建議用3.7到3.9太新的版本可能pyACL還不支持。我試過3.10有些CANN版本會報錯換回3.8就正常了。4. YOLO模型轉(zhuǎn)換從PyTorch到OM的完整實操4.1 導(dǎo)出ONNX模型的關(guān)鍵參數(shù)以YOLOv5為例導(dǎo)出ONNX的命令在models/export.py或者用export.py腳本。關(guān)鍵參數(shù)包括opset版本、輸入尺寸、是否簡化、是否動態(tài)batch。python export.py --weights yolov5s.pt --include onnx --opset 12 --img-size 640 640 --batch-size 1導(dǎo)出后用onnxruntime驗證一下ONNX模型能不能正常推理輸出shape是否符合預(yù)期。這一步很重要因為如果ONNX本身有問題ATC轉(zhuǎn)換肯定失敗。import onnxruntime as ort import numpy as np sess ort.InferenceSession(yolov5s.onnx) input_name sess.get_inputs()[0].name dummy np.random.randn(1, 3, 640, 640).astype(np.float32) outputs sess.run(None, {input_name: dummy}) print([o.shape for o in outputs])4.2 ATC轉(zhuǎn)換命令詳解ATC轉(zhuǎn)換是核心步驟命令參數(shù)比較多我挑幾個關(guān)鍵的講。atc --modelyolov5s.onnx \ --framework5 \ --outputyolov5s \ --input_formatNCHW \ --input_shapeimages:1,3,640,640 \ --logerror \ --soc_versionAscend310P3 \ --output_typeFP16 \ --precision_modeallow_fp32_to_fp16參數(shù)解釋--framework5表示ONNX--input_shape要和ONNX的輸入一致--soc_version要填對Atlas 300V是Ascend310P3填錯會轉(zhuǎn)換失敗或者推理異常--output_typeFP16表示輸出FP16Atlas 300V對FP16支持好性能比FP32高--precision_mode控制精度模式allow_fp32_to_fp16允許FP32算子降為FP16提升性能但可能影響精度。轉(zhuǎn)換成功后會生成yolov5s.om文件。如果轉(zhuǎn)換失敗日志里會提示哪個算子不支持這時候需要做算子替換或者用自定義算子。4.3 常見轉(zhuǎn)換錯誤和處理錯誤一Unsupported op type。比如YOLOv5的Focus層在早期版本里ATC不支持。解決辦法是把Focus層替換成等價的卷積層或者用YOLOv5的--focus參數(shù)導(dǎo)出時去掉Focus。錯誤二Shape inference failed。ONNX的某些動態(tài)shape導(dǎo)致ATC推斷失敗。解決辦法是在導(dǎo)出ONNX時固定所有shape不要用動態(tài)維度。錯誤三Precision loss too large。FP16精度損失太大導(dǎo)致輸出異常。解決辦法是設(shè)置--precision_modemust_keep_origin_dtype強制保持FP32但性能會下降。錯誤四soc_version mismatch。填錯了芯片型號。Atlas 300V是Ascend310P3Atlas 300I是Ascend310Atlas 800是Ascend910。填錯會轉(zhuǎn)換成功但推理失敗。注意ATC轉(zhuǎn)換時建議加--loginfo這樣能看到更多信息方便排查。但日志會很長轉(zhuǎn)換成功后可以改回error。5. 推理部署用pyACL把OM模型跑起來5.1 AscendCL推理的基本流程用pyACL做推理流程大概是初始化 → 加載模型 → 申請輸入輸出內(nèi)存 → 拷貝輸入數(shù)據(jù) → 執(zhí)行推理 → 獲取輸出 → 后處理 → 釋放資源。每一步都有對應(yīng)的API。import acl import numpy as np # 初始化 acl.init() acl.rt.set_device(0) # 加載模型 model_id, ret acl.mdl.load_from_file(yolov5s.om) # 獲取模型描述 model_desc acl.mdl.create_desc() acl.mdl.get_desc(model_desc, model_id) # 獲取輸入輸出數(shù)量 input_num acl.mdl.get_num_inputs(model_desc) output_num acl.mdl.get_num_outputs(model_desc)5.2 內(nèi)存申請和數(shù)據(jù)拷貝AscendCL的內(nèi)存分Host和Device兩種。輸入數(shù)據(jù)要先在Host上準備好然后拷貝到Device。輸出數(shù)據(jù)在Device上推理完成后拷貝回Host。# 申請Device輸入內(nèi)存 input_size acl.mdl.get_input_size_by_index(model_desc, 0) input_buffer acl.rt.malloc(input_size, acl.mem.MallocPolicy.ACL_MEM_MALLOC_HUGE_FIRST) # 準備輸入數(shù)據(jù) img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW img np.ascontiguousarray(img, dtypenp.float32) / 255.0 img np.expand_dims(img, axis0) # 拷貝到Device acl.rt.memcpy(input_buffer, input_size, img.tobytes(), input_size, acl.rt.memcpy_kind.ACL_MEMCPY_HOST_TO_DEVICE)5.3 執(zhí)行推理和獲取輸出推理用acl.mdl.execute傳入輸入輸出buffer的列表。執(zhí)行完成后輸出數(shù)據(jù)在Device上需要拷貝回Host。# 申請輸出內(nèi)存 output_buffers [] for i in range(output_num): size acl.mdl.get_output_size_by_index(model_desc, i) buf acl.rt.malloc(size, acl.mem.MallocPolicy.ACL_MEM_MALLOC_HUGE_FIRST) output_buffers.append((buf, size)) # 執(zhí)行推理 input_buffers [input_buffer] acl.mdl.execute(model_id, input_buffers, [b[0] for b in output_buffers]) # 拷貝輸出回Host outputs [] for buf, size in output_buffers: host_buf bytearray(size) acl.rt.memcpy(host_buf, size, buf, size, acl.rt.memcpy_kind.ACL_MEMCPY_DEVICE_TO_HOST) outputs.append(np.frombuffer(host_buf, dtypenp.float16))5.4 YOLO后處理從輸出張量到檢測框YOLOv5的輸出是三個尺度的特征圖shape分別是(1, 25200, 85)或者(1, 3, 80, 80, 85)等取決于導(dǎo)出時的設(shè)置。后處理包括解碼邊界框、置信度過濾、NMS。def non_max_suppression(prediction, conf_thres0.25, iou_thres0.45): # prediction: (1, 25200, 85) # 85 4 bbox 1 obj 80 cls output [] for pred in prediction: # 過濾低置信度 mask pred[:, 4] conf_thres pred pred[mask] if len(pred) 0: output.append(None) continue # 解碼 boxes xywh2xyxy(pred[:, :4]) scores pred[:, 4] * pred[:, 5:].max(axis1) classes pred[:, 5:].argmax(axis1) # NMS keep nms(boxes, scores, iou_thres) output.append(np.concatenate([boxes[keep], scores[keep, None], classes[keep, None]], axis1)) return output后處理部分建議用numpy實現(xiàn)不要用torch因為推理環(huán)境里不一定裝了torch。NMS可以用cv2.dnn.NMSBoxes也可以用numpy手寫性能差異不大。提示Atlas 300V輸出的數(shù)據(jù)類型可能是FP16后處理前要轉(zhuǎn)成FP32否則numpy計算可能出錯。另外輸出shape要和ATC轉(zhuǎn)換時的設(shè)置對應(yīng)如果轉(zhuǎn)換時改了輸出節(jié)點后處理也要相應(yīng)調(diào)整。6. 性能調(diào)優(yōu)讓Atlas 300V跑出應(yīng)有的速度6.1 影響性能的關(guān)鍵因素Atlas 300V 24G的算力在推理卡里算中上水平但實際性能取決于多個因素模型大小、輸入分辨率、batch size、精度模式、后處理效率、數(shù)據(jù)拷貝開銷。YOLOv5s在640x640輸入下FP16精度batch1Atlas 300V的單幀推理延遲大概在10到20毫秒之間具體看CANN版本和模型優(yōu)化程度。如果延遲明顯高于這個范圍說明有優(yōu)化空間。6.2 AIPP配置預(yù)處理加速AIPP是Ascend Image Pre-Processing的縮寫可以在推理前對圖像做硬件加速的預(yù)處理包括色域轉(zhuǎn)換、歸一化、裁剪等。配置AIPP后輸入數(shù)據(jù)可以是YUV或RGB的原始格式不需要在Host上做歸一化減少Host到Device的數(shù)據(jù)量和CPU開銷。AIPP配置通過一個配置文件指定ATC轉(zhuǎn)換時用--insert_op_conf參數(shù)加載。配置文件里定義輸入格式、輸出格式、歸一化參數(shù)等。{ aipp: { input_format: YUV420SP_U8, src_image_size_w: 640, src_image_size_h: 640, csc_switch: true, rbuv_swap_switch: false, mean_chn_0: 0, mean_chn_1: 0, mean_chn_2: 0, var_reci_chn_0: 0.003921568627, var_reci_chn_1: 0.003921568627, var_reci_chn_2: 0.003921568627 } }6.3 多batch和多線程推理如果業(yè)務(wù)允許增大batch size可以提升吞吐量。ATC轉(zhuǎn)換時把input_shape的batch維度設(shè)大推理時一次處理多幀。但要注意顯存限制batch太大可能OOM。多線程推理是另一個提升吞吐量的方法。Atlas 300V支持多stream可以創(chuàng)建多個推理stream每個stream跑一個模型實例并行處理。但要注意線程安全和內(nèi)存管理每個線程要有獨立的輸入輸出buffer。# 創(chuàng)建多個stream streams [] for i in range(4): stream, ret acl.rt.create_stream() streams.append(stream)6.4 性能測試和瓶頸定位性能測試建議用msprof工具可以采集推理過程中的時間消耗包括Host預(yù)處理、數(shù)據(jù)拷貝、Device推理、后處理各占多少。如果Device推理時間占比高說明模型本身計算量大如果數(shù)據(jù)拷貝時間占比高說明Host到Device的傳輸是瓶頸可以考慮用AIPP或者減少數(shù)據(jù)量。msprof --applicationpython infer.py --output./profiling注意性能調(diào)優(yōu)不要一次改太多參數(shù)每次改一個測一次記錄數(shù)據(jù)。否則出了問題不知道是哪個改動導(dǎo)致的。我習慣用表格記錄每次實驗的配置和結(jié)果方便對比。7. 常見問題與排查技巧實錄7.1 模型轉(zhuǎn)換類問題速查問題現(xiàn)象可能原因排查方法解決方案ATC報Unsupported op算子不支持看日志里哪個算子替換算子或自定義Shape inference failed動態(tài)shape檢查ONNX輸入shape固定所有維度轉(zhuǎn)換成功但推理報錯soc_version錯誤確認芯片型號改成Ascend310P3精度異常FP16損失大對比FP32輸出用must_keep_origin_dtype轉(zhuǎn)換時間過長模型太大看日志卡在哪簡化ONNX或分步轉(zhuǎn)換7.2 推理運行類問題速查問題現(xiàn)象可能原因排查方法解決方案初始化失敗驅(qū)動沒裝好npu-smi info重裝驅(qū)動加載模型失敗OM文件損壞檢查文件大小重新轉(zhuǎn)換推理結(jié)果全零輸入沒拷進去檢查memcpy確認數(shù)據(jù)格式顯存不足batch太大npu-smi查看減小batch推理速度慢沒用AIPPmsprof分析配置AIPP7.3 獨家避坑經(jīng)驗坑一ONNX的opset版本和ATC不匹配。我試過opset 15導(dǎo)出的ONNXATC直接報錯換成opset 12就正常了。建議先用opset 11或12穩(wěn)定后再嘗試更高版本??佣斎霐?shù)據(jù)的layout搞錯。PyTorch是NCHW但有些ONNX導(dǎo)出時是NHWCATC轉(zhuǎn)換時input_format要對應(yīng)。如果搞錯推理結(jié)果會完全亂掉但不會報錯很難發(fā)現(xiàn)。坑三后處理的NMS用了torch。推理環(huán)境里沒裝torchimport就失敗。后處理一定要用numpy或cv2實現(xiàn)不要依賴訓(xùn)練框架。坑四多線程推理時共享了buffer。多個線程同時寫同一個buffer結(jié)果互相覆蓋。每個線程要有獨立的buffer或者加鎖??游逋酸尫刨Y源。AscendCL的內(nèi)存和模型都要手動釋放否則跑久了會內(nèi)存泄漏。建議用try/finally確保釋放。try: # 推理代碼 pass finally: acl.rt.free(input_buffer) for buf, _ in output_buffers: acl.rt.free(buf) acl.mdl.unload(model_id) acl.rt.destroy_stream(stream) acl.rt.reset_device(0) acl.finalize()7.4 精度對齊的技巧從PyTorch到ONNX到OM精度會有損失。對齊精度的方法先用同樣的輸入跑PyTorch保存輸出再跑ONNX對比差異最后跑OM對比差異。如果OM和ONNX差異大說明ATC轉(zhuǎn)換有問題如果ONNX和PyTorch差異大說明導(dǎo)出有問題。對比時用np.allclose設(shè)置合理的rtol和atol。YOLO的輸出是浮點數(shù)允許一定誤差但如果誤差超過1e-2就要檢查了。提示Atlas 300V的FP16精度在某些算子上可能不夠如果業(yè)務(wù)對精度要求高可以用FP32模式但性能會下降。折中方案是混合精度關(guān)鍵層用FP32其他用FP16。8. 從部署到落地一些實際項目中的體會Atlas 300V部署YOLO這件事跑通demo和實際落地之間還有距離。跑通demo可能一兩天就能搞定但要在生產(chǎn)環(huán)境穩(wěn)定運行需要考慮的更多異常處理、日志記錄、性能監(jiān)控、模型更新、多卡調(diào)度等。我在實際項目里遇到過一個情況模型在測試集上精度正常但上線后某些場景下漏檢嚴重。排查后發(fā)現(xiàn)是輸入圖像的亮度分布和訓(xùn)練集差異大AIPP的歸一化參數(shù)需要調(diào)整。這件事讓我意識到部署不是簡單的模型轉(zhuǎn)換數(shù)據(jù)預(yù)處理的一致性同樣關(guān)鍵。另一個體會是昇騰生態(tài)的文檔和社區(qū)資料雖然不如CUDA豐富但官方文檔其實寫得挺細關(guān)鍵是你要有耐心去讀。很多問題在官方文檔的FAQ或者社區(qū)論壇里都有答案只是搜索需要技巧。建議遇到問題時先用英文關(guān)鍵詞搜昇騰社區(qū)再用中文搜往往能找到類似案例。最后分享一個小技巧ATC轉(zhuǎn)換時加--debug_dir參數(shù)可以把轉(zhuǎn)換過程中的中間文件保存下來包括算子映射關(guān)系、圖優(yōu)化前后的對比等。這些文件對排查轉(zhuǎn)換問題很有幫助尤其是算子不支持的時候能看到具體是哪個節(jié)點出了問題。這個內(nèi)容后續(xù)還可以這樣擴展如果你用的是YOLOv8導(dǎo)出ONNX的方式略有不同后處理也有變化如果你要做多模型串聯(lián)比如YOLO檢測加分類需要考慮模型間的數(shù)據(jù)傳遞和內(nèi)存復(fù)用如果你要在Atlas 200 DK這類邊緣設(shè)備上部署資源更受限優(yōu)化策略又不一樣。這些方向都值得單獨展開聊。