1? What,什么是孿生調試
Ascend C提供孿生調試方法,即CPU域模擬NPU域的行為,相同的算子代碼可以在CPU域調試精度,NPU域調試性能。孿生調試的整體方案如下:開發者通過調用Ascend C類庫編寫Ascend C算子kernel側源碼,kernel側源碼通過通用的GCC編譯器進行編譯,編譯生成通用的CPU域的二進制,可以通過gdb通用調試工具等調試手段進行調試;kernel側源碼通過畢昇編譯器進行編譯,編譯生成NPU域的二進制文件,可以通過msprof工具進行性能數據采集等方式進行調試。
針對NPU域的調試來講,根據依賴和調用動態庫的不同,分為NPU域仿真調試和NPU域上板調試。NPU域仿真調試,依賴和調用的是model仿真器指定的庫文件,運行model仿真不需要使用真實的NPU環境;上板調試,依賴和調用的是真實NPU環境上的庫文件,進行上板調試需要真實的NPU環境。
CPU域調試用于定位邏輯錯誤、內存錯誤等功能問題; NPU域調試不僅可以通過數據打印的方式定位功能問題,也可以用于定位性能問題、算子同步問題。
本文將從功能調試的角度出發,介紹CPU域和NPU域的調試方法,并通過具體的調試樣例來幫助大家快速掌握;性能調試的方法將在后續的文章中介紹。
2? HOW,如何進行調試
2.1? CPU域
本節介紹CPU域調試的兩種方法:gdb調試、使用printf打印命令打印。
2.1.1? gdb調試
gdb調試相信大家并不陌生,首先我們先來回顧幾個常用的調試命令,更多內容可以前往https://sourceware.org/gdb/深入學習。
命令 | 功能 |
step | 執行下一行語句,?如語句為函數調用,?進入函數中 |
next | 執行下一行語句,?如語句為函數調用,?不進入函數中 |
continue | 從當前位置繼續運行程序 |
run | 從頭開始運行程序 |
quit | 退出程序 |
| 輸出變量值、調用函數、通過表達式改變變量值 |
list | 查看當前位置代碼 |
backtrace | 查看各級堆棧的函數調用及參數 |
break?N | 在第N行上設置斷點 |
display | 每次停下來時,顯示設置的變量var的值 |
這里稍微復雜一點的是,因為我們程序是多核執行程序,cpu調測將其轉為多進程調試,每個核都會拉起獨立的子進程,故gdb需要轉換成子進程調試的方式。下面介紹子進程調試的方法。
- 調試單獨一個子進程
在gdb啟動后,首先設置跟蹤子進程,之后再打斷點,就會停留在子進程中,設置的命令為:
set follow-fork-mode child
但是這種方式只會停留在遇到斷點的第一個子進程中,其余子進程和主進程會繼續執行直到退出。涉及到核間同步的算子無法使用這種方法進行調試。
如下是調試一個單獨子進程的調試命令樣例:
gdb --args add_custom_cpu
set follow-fork-mode child
break add_custom.cpp:45
run
list
backtrace
print i
break add_custom.cpp:56
continue
display xLocal
quit
- 調試多個子進程
如果涉及到核間同步,那么需要能同時調試多個子進程。
在gdb啟動后,首先設置調試模式為只調試一個進程,掛起其他進程。設置的命令如下:
(gdb) set detach-on-fork off
查看當前調試模式的命令為:
??????(gdb) show detach-on-fork
中斷gdb程序的方式要使用捕捉事件的方式,即gdb程序監控fork這一事件并中斷。這樣在每一次起子進程時就可以中斷gdb程序。設置的命令為:
(gdb) catch fork
當執行r后,可以查看當前的進程信息:
(gdb) info inferiors
??Num ?Description
* 1 ???process 19613
可以看到,當第一次執行fork的時候,程序斷在了主進程fork的位置,子進程還未生成。
執行c后,再次查看info inferiors,可以看到此時第一個子進程已經啟動。
(gdb) info inferiors
??Num ?Description
* 1 ???process 19613
??2 ???process 19626
這個時候可以使用切換到第二個進程,也就是第一個子進程,再打上斷點進行調試,此時主進程是暫停狀態:
(gdb) inferior 2
[Switching to inferior 2 [process 19626] ($HOME/demo)]
(gdb) info inferiors
??Num ?Description
??1 ???process 19613
* 2 ???process 19626
請注意,inferior后跟的數字是進程的序號,而不是進程號。
如果遇到同步阻塞,可以切換回主進程繼續生成子進程,然后再切換到新的子進程進行調試,等到同步條件完成后,再切回第一個子進程繼續執行。
2.1.2? printf打印命令
printf則更為簡單,直接使用printf打印命令printf(...)來觀察數值的輸出。需要注意的是:NPU模式下目前不支持打印語句,所以需要添加內置宏__CCE_KT_TEST__予以區分。樣例代碼如下:
printf("xLocal size: %d\n", xLocal.GetSize());
printf("tileLength: %d\n", tileLength);
2.2? NPU域????????
2.2.1? 上板數據打印(DumpTensor、PRINTF)
功能包括DumpTensor、PRINTF兩種,其中DumpTensor用于打印指定Tensor的數據,PRINTF主要用于打印標量和字符串信息。
具體的使用方法如下:
1. 設置打開DUMP開關的環境變量
export ACL_DUMP_DATA=1
修改Dump信息配置文件acl.json。單算子調用應用開發場景下,新建acl.json放在應用開發的工程目錄下,在調用aclInit接口時傳入;Pytorch調用場景下,放在Pytorch腳本執行目錄下,由框架自行讀取。配置樣例如下:其中dump_path表示dump數據文件存儲到運行環境的目錄,支持配置絕對路徑或相對路徑;dump_mode表示dump的模式,配置成input/output/all有效模式均可以;dump_op_switch表示單算子dump數據開關,需要配置成on,開啟單算子dump模式;dump_debug為預留參數,開發者無需關注,直接配置成off即可。
{
????"dump":{
????????"dump_path":"/dump",
????????"dump_mode": "all",
????????"dump_debug": "off",
????????"dump_op_switch": "on"
?????}
}
2. 增加算子工程編譯選項-DASCENDC_DUMP:修改算子工程op_kernel目錄下的CMakeLists.txt文件,首行增加編譯選項,打開DUMP開關,樣例如下:
add_ops_compile_options(ALL OPTIONS -DASCENDC_DUMP)
3.?在算子kernel側實現代碼中需要輸出日志信息的地方調用DumpTensor接口或者PRINTF接口打印相關內容
a)?DumpTensor示例,srcLocal表示待打印的Tensor;5表示用戶的自定義附加信息,比如當前的代碼行號;dataLen表示元素個數。
DumpTensor(srcLocal,5, dataLen);
Dump時,每個block核的dump信息前會增加對應信息頭DumpHead(32字節大小),用于記錄核號和資源使用信息;每次Dump的Tensor數據前也會添加信息頭DumpTensorHead(32字節大小),用于記錄Tensor的相關信息。如下圖所示,展示了多核打印場景下的打印信息結構。
DumpHead的具體信息如下:
- block_id:當前運行的核號;
- total_block_num:此次dump的核數;
- block_remain_len:當前核剩余可用的dump的空間;
- block_initial_space:當前核初始分配的dump空間;
- magic:內存校驗魔術字。
DumpTensorHead的具體信息如下:
- desc:用戶自定義附加信息;
- addr:Tensor的地址;
- data_type:Tensor的數據類型;
- position:表示Tensor所在的物理存儲位置。
打印結果的樣例如下:
DumpHead: block_id=0, total_block_num=16, block_remain_len=1048448, block_initial_space=1048576, magic=5aa5bccd
DumpTensor: desc=5, addr=0, data_type=DT_FLOAT16, position=UB
[40, 82, 60, 11, 24, 55, 52, 60, 31, 86, 53, 61, 47, 54, 34, 62, 84, 29, 48, 95, 16, 0, 20, 77, 3, 55, 69, 73, 75, 40, 35, 13]
DumpHead: block_id=1, total_block_num=16, block_remain_len=1048448, block_initial_space=1048576, magic=5aa5bccd
DumpTensor: desc=5, addr=0, data_type=DT_FLOAT16, position=UB
[58, 84, 22, 54, 41, 93, 1, 45, 50, 9, 72, 81, 23, 96, 86, 45, 36, 9, 36, 34, 78, 7, 2, 29, 47, 26, 13, 24, 27, 55, 90, 5]
...
DumpHead: block_id=7, total_block_num=16, block_remain_len=1048448, block_initial_space=1048576, magic=5aa5bccd
DumpTensor: desc=5, addr=0, data_type=DT_FLOAT16, position=UB
[28, 27, 79, 39, 86, 5, 23, 97, 89, 5, 65, 69, 59, 13, 49, 2, 34, 6, 52, 38, 4, 90, 11, 11, 61, 50, 71, 98, 19, 54, 54, 99]
b)?PRINTF示例如下:
PRINTF("fmt string %d", 0x123);
3? Example,調試樣例
下面通過具體的樣例來實戰一下吧!
3.1? CPU域調試樣例
在進行調試之前我們需要獲取一個精度有問題的算子樣例
1. 通過如下的樣例鏈接獲取正確的樣例。(CPU調試當前僅適用于通過內核調用符調用算子的程序調試,所以這里我們獲取KernelLaunch的代碼樣例)
https://gitee.com/ascend/samples/tree/master/operator/AddCustomSample/KernelLaunch
2. 將AddKernelInvocation / add_custom.cpp中的Init函數替換成如下有bug的代碼。
__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z)
????{
????????xGm.SetGlobalBuffer((__gm__ half*)x + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);
????????yGm.SetGlobalBuffer((__gm__ half*)y + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);
????????zGm.SetGlobalBuffer((__gm__ half*)z + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);
????????pipe.InitBuffer(inQueueX, BUFFER_NUM, TILE_LENGTH);
????????pipe.InitBuffer(inQueueY, BUFFER_NUM, TILE_LENGTH);
????????pipe.InitBuffer(outQueueZ, BUFFER_NUM, TILE_LENGTH);
}
3. 參考樣例readme,跑一下樣例,樣例出現如下報錯:
[ERROR] result error
下面我們一起開始debug之旅吧。
- 觀察日志報錯。
觀察是否有打屏日志報錯,可搜索關鍵詞"failed"。下圖的報錯示例指示,錯誤出現在代碼中調用Add接口的地方。
add_cpu: /usr/local/Ascend/ascend-toolkit/7.0.RC1.alpha003/x86_64-linux/tikcpp/tikcfw/interface/kernel_operator_vec_binary_intf.h:79:?void AscendC::Add(const AscendC::LocalTensor<T>&, const AscendC::LocalTensor<T>&, const AscendC::LocalTensor<T>&, const int32_t&) [with T = float16::Fp16T; int32_t = int]: Assertion `false && "check vadd instr failed"' failed.
通過上述報錯日志,一般只能定位到框架報錯的代碼行,無法明確具體錯誤,接下來需要通過gdb調試的方式或者printf打印的方式進一步精確定位。
2. gdb調試。下面的樣例展示了拉起Add算子CPU側運行程序的樣例,該樣例程序會直接拋出異常,直接gdb運行,查看調用棧信息分析定位即可。其他場景下您可以使用gdb打斷點等基本操作進行調試。
????????1) 使用gdb拉起待調試程序,進入gdb界面進行debug。
gdb add_cpu
? ? ? ? 2) 單獨調試一個子進程。
(gdb) set follow-fork-mode child
? ? ? ? 3) 運行程序。
(gdb) r
? ? ? ? 4) 通過bt查看程序調用棧。
(gdb) bt
? ? ? ? 5) 查看具體層的堆棧信息,打印具體變量的值。本示例中,查看報錯代碼的上一層第5層的堆棧,打印了TILE_LENGTH為128,該程序中表示需要處理128個half類型的數,大小為128*sizeof(half)=256字節;同時打印了輸入Tensor xLocal的值,其中dataLen表示LocalTensor的size大小為128字節,只能計算128字節的數據。可以看出兩者的長度不匹配,由此可以繼續檢查代碼內存分配時傳入長度是否有誤,從而定位到Init函數中InitBuffer的問題。同理,大家可以自行打印yLocal和zLocal的值。
(gdb) f 5
#5 ?0x0000555555560638 in KernelAdd::Compute (this=0x7fffffffd360, progress=0)
????at /samples-master/operator/AddCustomSample/KernelLaunch/AddKernelImpl/add.cpp:54
54 ?????????????Add(zLocal, xLocal, yLocal, TILE_LENGTH); (gdb) p TILE_LENGTH
$1 = 128
(gdb) p xLocal
$3 = {<AscendC::BaseTensor<float16::Fp16T>> = {<No data fields>}, address_ = {logicPos = 9 '\t', bufferHandle = 0x7fffffffd460 "\003\005\377\377\200", dataLen = 128,
bufferAddr = 0,absAddr = …}
3. printf打印。在調用報錯代碼行之前的位置增加變量打印。樣例代碼如下:
__aicore__ inline void Compute(int32_t progress)
????{
????????LocalTensor<half> xLocal = inQueueX.DeQue<half>();
????????LocalTensor<half> yLocal = inQueueY.DeQue<half>();
????????LocalTensor<half> zLocal = outQueueZ.AllocTensor<half>();
????????#ifdef __CCE_KT_TEST__
????????printf("xLocal size: %d\n", xLocal.GetSize());
printf("tileLength: %d\n", TILE_LENGTH);
#endif
????????Add(zLocal, xLocal, yLocal, TILE_LENGTH);
????????outQueueZ.EnQue<half>(zLocal);
????????inQueueX.FreeTensor(xLocal);
????????inQueueY.FreeTensor(yLocal);
????}
可以看到有如下打屏日志輸出,打印了tileLength為128,該程序中表示需要處理128個half類型的數;輸入Tensor xLocal的size大小,為64,表示只能計算64個half類型的數。可以看出兩者的長度不匹配,由此可以繼續檢查代碼內存分配時傳入長度是否有誤,從而定位到Init函數中InitBuffer的問題。
xLocal size: 64
tileLength: 128
3.2? NPU域調試樣例
在進行調試之前我們需要獲取一個精度有問題的算子樣例
- 通過如下的樣例鏈接獲取正確的樣例。(上板數據打印調試當前適用于單算子調用程序(aclnn接口)調試,所以這里我們獲取算子工程和aclnn單算子調用的代碼樣例)
???????samples: CANN Samples - Gitee.com
2. 將AddCustom / op_kernel / add_custom.cpp中的Init函數替換成如下有bug的代碼。
__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z, uint32_t totalLength, uint32_t tileNum)
????{
????????ASSERT(GetBlockNum() != 0 && "block dim can not be zero!");
????????this->blockLength = totalLength / GetBlockNum();
????????this->tileNum = tileNum;
????????ASSERT(tileNum != 0 && "tile num can not be zero!");
????????this->tileLength = this->blockLength / tileNum / BUFFER_NUM;
????????xGm.SetGlobalBuffer((__gm__ half*)x + this->blockLength * GetBlockIdx(), this->blockLength);
????????yGm.SetGlobalBuffer((__gm__ half*)y + this->blockLength * GetBlockIdx(), this->blockLength);
????????zGm.SetGlobalBuffer((__gm__ half*)z + this->blockLength * GetBlockIdx(), this->blockLength);
????????pipe.InitBuffer(inQueueX, BUFFER_NUM, this->tileLength);
????????pipe.InitBuffer(inQueueY, BUFFER_NUM, this->tileLength);
????????pipe.InitBuffer(outQueueZ, BUFFER_NUM, this->tileLength);
????}
3. 參考樣例readme,跑一下算子的編譯部署以及aclnn調用樣例,aclnn調用樣例出現如下報錯:
[ERROR] result error
下面我們一起開始debug之旅吧。
在AddCustom / op_kernel / add_custom.cpp中關鍵的流程中增加數據打印
比如如下的示例代碼分別在計算前和計算后增加對zLocalTensor的數據打印,同時打印參與計算的元素個數。
DumpTensor(zLocal, __LINE__, zLocal.GetSize());
????????PRINTF("The data Length involved in calculation is %d.\n", this->tileLength);
????????Add(zLocal, xLocal, yLocal, this->tileLength);
????????DumpTensor(zLocal, __LINE__, zLocal.GetSize());
參考上文的上板數據打印流程進行環境變量、cmake文件等配置,運行aclnn單算子調用樣例,可以看到屏幕有如下打印:
DumpTensor: desc=54, addr=512, data_type=DT_FLOAT16, position=UB
[59.1875, 66.625, 53.4375, 52.0625, 80.3125, 10.5234, 21.3594, 43.2188, 1.15039, 50.625, 7.88672, 60.0625, 63.9062, 28.2812, 6.72266, 66.3125, 38.9062, 7.9375, 37.8125, 38.9062, 57.0938, 14.2266, 66.625, 62.3125, 5.17969, 1.63477, 45.2812, 68.1875, 59.9375, 23.8281, 71.8125,
45.1875, 59.2188, 53.875, 77.25, 47.5625, 75, 38.1875, 61.2812, 63.125, 58.5312, 83.6875, 44.5312, 57.125, 2.74609, 32.8438, 27.1094, 20.9219, 70.3125, 62.7188, 20, 6.90625, 30.5312, 91.75, 17.3125, 29.8125, 68.9375, 83.125, 23.4375, 58.8125, 62.6875,
69.9375, 19.9531, 46.25]
The data Length involved in calculation is 128.
DumpTensor: desc=57, addr=512, data_type=DT_FLOAT16, position=UB
[151.75, 158, 48.125, 96.25, 158, 74.125, 101.375, 34, 36.2812, 112.375, 109.125, 95.25, 142.75, 44.75, 176.375, 130.125, 68.3125, 70.3125, 102.375, 92.75, 139.25, 55.4375, 87, 102.75, 177.375, 118.875, 113.375, 25.6562, 101.125, 107.75, 165.5,
72.5, 17.5625, 52.4688, 167.125, 132.75, 171.375, 98.75, 133.375, 107.5, 119.375, 149.25, 161.625, 146.375, 162.25, 88.8125, 119.375, 88.9375, 170.5, 91.625, 72.9375, 131.875, 71.5625, 44.6562, 57.0625, 101.5, 150.125, 71.75, 126.625, 113.688, 139.75,
107.938, 79.5625, 132.875]
觀察得到tensor的長度為64,但是設置的計算長度為128 兩者不匹配,進一步定位可以確定問題為Init時初始化buffer的長度錯誤。
通過本篇內容 大家可以了解Ascend?C孿生調試的概念,并可以參照實際樣例進行實戰練習。更多相關內容請參考:
《Ascend C 官方教程》