華為Atlas Python多程序應用開發

華為的Atlas裝置搭載了昇騰310晶片,這是華為海思設計的一款SOC。這塊晶片不止包含了用於加速神經網路運算的NPU,還有用於多媒體解碼的加速硬體、以及用於任務協調排程的CPU(華為叫做AICPU)。這塊晶片可以將CPU從運算繁重的任務中解放出來,但是同時這給應用開發也帶來了一些挑戰。你可以從上一篇文章 華為Atlas裝置影象裁切 中體會到涉及昇騰晶片應用開發的不同之處。本篇文章則描述了使用Python語言在Atlas裝置上實現多程序應用開發技術坑以及解決方式。

‌ ‌

常規多程序影片處理

假設你要開發一款影片分析應用,需要同時接入兩路影片,且兩路畫面都需要人臉檢測功能。為了充分利用裝置資源,影片解碼要在昇騰晶片上實現。一種可能的應用設計如下:

華為Atlas Python多程序應用開發

兩個程序實現

該設計包含了兩個程序,程序1負責接入影片與影象解碼;程序2負責人臉檢測。兩個程序透過佇列在記憶體中交換資料。在不使用昇騰硬體的環境下,你可以使用這段程式碼實現:

“”“Template code for video frame processing。”“” import multiprocessing as mp import cv2 BUFFER_SIZE = 12 RUNNING = mp。Value(“i”, 1) def get_frames(srcs, q: mp。Queue): “”“子程序物件。用於影片流解碼,並將解碼後的影象填充到佇列中。”“” caps = [cv2。VideoCapture(src) for src in srcs] print(“Opening streams。。。”) while RUNNING。value == 1: for i, cap in enumerate(caps): _, frame = cap。read() q。put((frame, i)) print(“Streaming stopped。”) def main(): # 設定影片源 video_srcs = [‘/home/robin/Videos/pose。mp4’, ‘/home/robin/Videos/mask。mp4’, ] # 構建用於交換資料的佇列 frame_q = mp。Queue(maxsize=BUFFER_SIZE) # 構建程序並啟動它 p = mp。Process(target=get_frames, args=(video_srcs, frame_q,)) p。start() # 預覽影象。 while RUNNING。value == 1: frame, cap_id = frame_q。get() frame = cv2。resize(frame, (0,0), fx=0。5, fy=0。5) cv2。imshow(‘{}’。format(cap_id), frame) if cv2。waitKey(1) == 27: RUNNING。value = 0 print(“Closing queue。。。”) frame_q。close() print(“Joining process。。。”) p。terminate() print(“All done。”) if __name__ == “__main__”: main()

兩個程序實現影片解碼與處理

這段程式碼在執行時會在兩個獨立的預覽視窗顯示解碼後的影象。

華為Atlas Python多程序應用開發

兩路影片預覽畫面

一切看上去都很和諧。但是,當你引入昇騰晶片後,事情發生了變化。

硬體加速的影片解碼

影片解碼是典型的CPU繁重型任務。昇騰310晶片中有專門的硬體模組可以實現H。264/265硬體加速解碼。我們可以藉助該硬體模組來減輕CPU負擔,加快應用執行速度。

在上一篇關於影象裁切的文章中,你已經透過昇騰應用開發的Python SDK pyACL實現了影象的區域裁切。影片解碼的開發過程與其非常類似,充斥著大量有著“奇怪”名字的API。在這裡我們將引入一款由華為FAE工程師進一步封裝後的Python SDK:AscendFly。它將常用功能封裝為模組,然後對外提供類似OpenCV風格的介面,可以有效提升開發效率。

ascend-fae/ascendfly

ascend目標檢測分類框架

Gitee ascend-fae

AscendFly官方程式碼

AscendFly提供了影片功能抽象模組

ascend。VideoCapture

來實現影片檔案的讀取與解碼。它的使用方法與OpenCV的

VideoCapture

非常相似。

# 匯入ascend模組 import ascend # 構建昇騰硬體抽象 context = ascend。Context({0})。context_dict[0] # 構建影片處理物件 cap = ascend。VideoCapture(context, video_file, channel_id) # 讀取影片幀 frame, frame_id = cap。read()

ascend影片解碼

相比pyACL提供的API,經過AscendFly封裝後的模組顯然更加友好。同理,我們可以將之前已經寫好的程式碼稍作改造,讓它實現硬體加速的影片解碼。

“”“Template code for video frame processing。”“” import multiprocessing as mp import ascend import cv2 DEVICE_ID = 0 BUFFER_SIZE = 12 RUNNING = mp。Value(“i”, 1)def get_frames(srcs, q: mp。Queue): “”“子程序物件。用於影片流解碼,並將解碼後的影象填充到佇列中。”“” ctx = ascend。Context({DEVICE_ID})。context_dict[DEVICE_ID] caps = [ascend。VideoCapture(ctx, v, i+1) for i, v in enumerate(srcs)] print(“Opening streams。。。”) while RUNNING。value == 1: for i, cap in enumerate(caps): if cap。is_open(): yuv, _ = cap。read(False) if yuv is not None: bgr = cv2。cvtColor(yuv。to_np, cv2。COLOR_YUV2BGR_NV12) q。put((bgr, i)) print(“Streaming stopped。”) def main(): # 設定影片源 video_srcs = [‘/home/robin/Videos/pose。mp4’, ‘/home/robin/Videos/mask。mp4’, ] # 構建用於交換資料的佇列 frame_q = mp。Queue(maxsize=BUFFER_SIZE) # 構建程序並啟動它 p = mp。Process(target=get_frames, args=(video_srcs, frame_q,)) p。start() # 預覽影象。 while RUNNING。value == 1: frame, cap_id = frame_q。get() frame = cv2。resize(frame, (0,0), fx=0。5, fy=0。5) cv2。imshow(‘{}’。format(cap_id), frame) if cv2。waitKey(1) == 27: RUNNING。value = 0 print(“Closing queue。。。”) frame_q。close() print(“Joining process。。。”) p。terminate() print(“All done。”) if __name__ == “__main__”: main()

與之前的程式碼相比,這段程式碼的主要改動發生在

get_frames

函式中:使用

ascend。VideoCapture

替換了OpenCV的

VideoCapture

。同時為了讓硬體解碼正常工作,在子程序的建立函式中初始化了昇騰硬體資源抽象context。另外需要注意的是昇騰硬體解碼後的影象格式為YUV420SP。第25行程式碼將其轉換為OpenCV BGR格式。

至此,你已經成功在一個子程序中啟用了昇騰晶片的硬體解碼加速功能。接下來,你需要在另一個程序中實現人臉檢測功能。

NPU加速的人臉檢測

與影片解碼不同,人臉檢測需要呼叫神經網路模型加速硬體NPU。你也許已經猜到了,這個呼叫過程同樣離不開昇騰晶片的硬體抽象。一個典型的呼叫方式大概是這樣:

# 匯入ascendfly import ascend # 構建硬體抽象 context = ascend。Context({0})。context_dict[0] # 初始化模型 model = ascend。AscendModel(context, ‘detector。om’)

離線模型的構建方式

雖然你已經在

get_frames

函式中構建了一個context,但是由於人臉檢測模型與影片解碼不在同一個程序下,如何實現context的共用?

一種方法是將

get_frames

函式中的context構建刪掉,在主程序中構建context,然後將其作為引數傳入子程序。修改內容大致如下:

# 用於子程序建立的函式 def get_frames(context, src, q: mp。Queue): cap = ascend。VideoCapture(context, src) # 在main函式中 def main(): context = ascend。Context({0})。context_dict[0] p = mp。Process(target=get_frames, args=(context, video_src, frame_q,))

將context作為引數傳遞給子程序

但是,這種傳遞方式是無效的。這裡的昇騰硬體抽象context是與程序繫結在一起的。它無法作為一個引數傳遞給另一個程序去使用。那麼分別在兩個程序中建立各自的context呢?

# 用於子程序建立的函式 def get_frames(src, q: mp。Queue): context = ascend。Context({0})。context_dict[0] cap = ascend。VideoCapture(context, src) # 在main函式中 def main(): context = ascend。Context({0})。context_dict[0] p = mp。Process(target=get_frames, args=(context, video_src, frame_q,))

在兩個程序中分別構建自己的context

這個思路是正確的。但是,像上方這樣的實現,會導致程序掛起,陷入永久的等待之中。而要解決這個問題,需要先從Python下的程序建立說起。

Spawn與Fork

程序是計算機中一系列運算操作的集合,這個小集合共享當前程序所擁有的運算資源。從現有程序派生出的新程序叫作子程序。在Linux下,Python支援三種程序派生方式,其中較為常用的有兩個:Spawn與Fork。

Spawn的字面意思是魚、蛙產卵。根據Python官方文件,採用這種方式建立的子程序,只繼承了執行所需的部分必要資源,就像是上級程序“生”出來的一樣,而非上級程序的克隆。像是檔案運算子等資源不會被繼承。

華為Atlas Python多程序應用開發

影象來源 Rachel Hisko

Fork的字面意思是叉子。以這種方式建立的子程序從上級程序中繼承了全部資源,就像是一把叉子的末端分叉出一模一樣的多個分叉。在分叉的起始點它們是一模一樣的。

華為Atlas Python多程序應用開發

子程序也是程序,這意味著它也會有屬於自己的運算資源。但是由於派生方式的不同,導致它們在派生之後擁有的運算資源存在差異。這時候回想一下,前文中在使用昇騰晶片的硬體加速模組時,必須要為其建立的硬體抽象context也是一種資源。讀到這裡你也許已經猜到了,程序掛起多半是與硬體抽象context的繼承方式有關。

的確如此。根據華為工程師的說明,

昇騰硬體資源的跨程序繼承當前只支援Spawn方式

。而根據Python官方文件,在Linux下,程序的預設派生方式為Fork。所以,前述程式碼中未設定子程序派生方式的時候,Python以Fork的形式建立了子程序,子程序中繼承的升騰運算資源無法正常使用,這是導致程序掛起的原因。而其解決方式也很自然:

以Spawn的形式派生子程序。

在子程序中初始化新的昇騰運算抽象資源。

經測試,使用這種方式可以實現Python下的多程序昇騰加速運算。

總結

使用Python開發華為Atlas多程序應用時,需要注意昇騰開發SDK當前僅支援以Spawn的方式派生新程序。在Linux下程序的預設派生方式為Fork,這可能會導致程序掛起失去響應。顯式的指定Spawn派生方式,並在子程序中初始化昇騰硬體抽象即可解決這個問題。