日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

Kaldi内存泄漏问题排查

發(fā)布時(shí)間:2025/3/21 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Kaldi内存泄漏问题排查 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

轉(zhuǎn)載自:https://www.baidu.com/link?url=uUnBEi2XoXwkMYf_mLzKuZmdz8auQ5mjvwYE0c5zsKS2kUcEMv3fo9wUmva2S84mX9GcchAup9S_y9iYp1OzBz0rhERJS3QImQNQxrmhTx7&wd=&eqid=b4847b6b0000c23c000000065fb21f0c

2019-08-05

賓狗

?工程

?Kaldi??內(nèi)存泄漏


  • 0x00 情況概述
  • 0x01 排查過(guò)程
    • valgrind的簡(jiǎn)單使用
    • 正確釋放STL當(dāng)中vector的內(nèi)存
    • 內(nèi)存碎片無(wú)法釋放
    • 多線程中的陷阱
  • 0x02 更進(jìn)一步?
  • 參考資料

最近在做Kaldi相關(guān)開(kāi)發(fā)的過(guò)程中,遇到了一個(gè)非常棘手的內(nèi)存問(wèn)題,現(xiàn)將整個(gè)排查解決過(guò)程梳理一下,希望對(duì)有類似問(wèn)題的同學(xué)有幫助。

0x00 情況概述

Kaldi是一個(gè)語(yǔ)音識(shí)別的C++開(kāi)發(fā)框架,集成了非常多的工具和模塊。由于項(xiàng)目需要,希望能夠?qū)VTE開(kāi)源的模型部署到內(nèi)部線上測(cè)試使用,且能夠充分利用GPU加速,而網(wǎng)上的教程大多都是基于offline模式,使用的是nnet3和nnet3bin下面的模塊和程序。

當(dāng)然其中nnet3bin也有一個(gè)使用了GPU顯卡的示例程序nnet3-latgen-faster-batch,但它并不是一個(gè)充分利用GPU計(jì)算的實(shí)現(xiàn),整體還是比較低效的。NVIDIA的工程師在今年GTC19上開(kāi)源了他們的實(shí)現(xiàn),但是想要將CVTE模型在這個(gè)版本的實(shí)現(xiàn)上跑起來(lái),需要定制化的做一些開(kāi)發(fā),具體的hacking過(guò)程就按下不表,我們的主題是排查內(nèi)存的泄漏問(wèn)題。程序運(yùn)行起來(lái)之后,一個(gè)很顯著的問(wèn)題是內(nèi)存占用太高了,在原來(lái)的nnet3-latgen-faster-batch下占用內(nèi)存差不多在17G左右,不會(huì)超過(guò)20G,而在使用了NVIDIA這個(gè)版本后,內(nèi)存占用一度超過(guò)了35G,峰值甚至在40G以上,而且隨著需要識(shí)別音頻不斷被輸入進(jìn)來(lái),內(nèi)存占用還在緩慢的上升。所以,非常有必要解決其中隱藏的內(nèi)存問(wèn)題。

0x01 排查過(guò)程

經(jīng)過(guò)一番搜索,在Linux下常用的內(nèi)存泄露檢查工具箱是valgrind,這是一個(gè)非常強(qiáng)大的工具,光說(shuō)明使用手冊(cè)就有400多頁(yè)。

比較傻瓜的使用方式是直接使用valgrind下面的memcheck工具,當(dāng)然這個(gè)工具并不是萬(wàn)能的,在Kaldi這種比較復(fù)雜龐大的工程下面,想要定位出問(wèn)題所在并不容易;所以我所使用的工具是massif,它能夠在程序執(zhí)行的過(guò)程中截取快照,記錄每個(gè)快照當(dāng)中,程序內(nèi)存的詳細(xì)使用情況,精確到哪個(gè)模塊的哪一行代碼申請(qǐng)占用內(nèi)存

valgrind的簡(jiǎn)單使用

安裝配置過(guò)程可參考這個(gè)鏈接,如果沒(méi)有權(quán)限的話,安裝到自己目錄后配置好環(huán)境變量即可使用

執(zhí)行命令

# 說(shuō)明 # ./后面可以跟你需要tracking內(nèi)存的程序及其參數(shù) # 注意這個(gè)模式下只tracking堆內(nèi)存,如果想要記錄所有內(nèi)存情況,需要加上一個(gè)--page-as-heap選項(xiàng) # threshold表示追蹤的最低的內(nèi)存百分比,例如設(shè)為1.0表示占比在1.0%以下的內(nèi)存就不記錄了 # time-unit是一個(gè)比較迷的參數(shù),影響的是最后畫出內(nèi)存變化趨勢(shì)圖,可選單位的有時(shí)間(ms)、指令數(shù)(默認(rèn))、內(nèi)存量(B) # massif-out-file指定輸出的文件名 valgrind --tool=massif --threshold=0.0 --time-unit=ms --massif-out-file=memory_footprint ./batched-wav-nnet3-cuda config.yaml# valgrind提供了一個(gè)命令行版的可視化工具,可以簡(jiǎn)單查看剛才的tracking結(jié)果文件 # 如果使用的是帶可視化界面的Linux,可以使用相應(yīng)的圖形化工具 # 這里我們把結(jié)果輸出到一個(gè)文本文件里面,以便后續(xù)分析 ms_print memory_footprint > vis.txt

基本的情況如下所示,可以看到堆內(nèi)存隨時(shí)間的變化情況,下面還有詳細(xì)的每個(gè)snapshot內(nèi)部的內(nèi)存分部情況

一個(gè)詳細(xì)的snapshot內(nèi)存記錄如下所示:

只要在編譯C++程序的時(shí)候加上了-g選項(xiàng),就可以從這里詳細(xì)的看到是程序的哪一行代碼申請(qǐng)占用內(nèi)存;例如上圖有好處幾resize函數(shù),那么很顯然就是std::vector里面的resize函數(shù)申請(qǐng)占用內(nèi)存

正確釋放STL當(dāng)中vector的內(nèi)存

在這個(gè)工具的幫助下,我很快定位到了一類比較常見(jiàn)的問(wèn)題。只需重點(diǎn)關(guān)注std::vector的reserve和resize這兩個(gè)函數(shù),如果某行代碼通過(guò)這種方式申請(qǐng)了內(nèi)存,在后續(xù)的穩(wěn)定運(yùn)行過(guò)程中又沒(méi)有用到,且在程序快結(jié)束的時(shí)候依然占用著,那么這就是一個(gè)可以改進(jìn)的點(diǎn)了。

這類問(wèn)題網(wǎng)上一搜就能找到很多,主要是vector的STL實(shí)現(xiàn)當(dāng)中函數(shù)名太有誤導(dǎo)性了

std::vector<obj> test = ...; ... test.clear();

很多人以為調(diào)用了clear函數(shù)之后,其占用內(nèi)存空間就自然而然的釋放掉了,其實(shí)不然,vector當(dāng)中有一個(gè)capacity的概念,調(diào)用clear函數(shù)只是使用vector的size變?yōu)榱?,而其capacity沒(méi)有變化,也就是所占用內(nèi)存沒(méi)有發(fā)生變化,正確的釋放方式如下:

std::vector<obj>().swap(test);

當(dāng)然,在C++11當(dāng)中,我們又多了一種選擇

test.clear(); test.shrink_to_fit();

在NVIDIA工程師的實(shí)現(xiàn)當(dāng)中,有好幾處類似這種沒(méi)有正確釋放vector內(nèi)存的情況,一一更正之后內(nèi)存占用下降到了30G左右。由于這部分不是本文的重點(diǎn),具體的截圖就不貼出來(lái)了。

內(nèi)存碎片無(wú)法釋放

接下來(lái)的這個(gè)問(wèn)題就非常詭異了,為了說(shuō)清楚情況,我先簡(jiǎn)單介紹一下NVIDIA這個(gè)GPU實(shí)現(xiàn)和原來(lái)的相比有什么區(qū)別改進(jìn)。

語(yǔ)音識(shí)別的流程大體上可以分為三個(gè)步驟(本人非專家,可能不嚴(yán)謹(jǐn),只是從代碼的角度敘述),第一步是計(jì)算抽取聲學(xué)特征(features),第二步是根據(jù)聲學(xué)特征inference(NBatchCompute),第三步是解碼過(guò)程(decode),這個(gè)過(guò)程比較復(fù)雜也是最耗時(shí)耗內(nèi)存的部分。在解碼的時(shí)候有用到一個(gè)非常大的文件HCLG.fst,可以把它理解為一個(gè)非常龐大的狀態(tài)轉(zhuǎn)移圖,解碼的過(guò)程就是在這個(gè)狀態(tài)圖當(dāng)中搜索的過(guò)程。

在原來(lái)的非GPU版本中,kaldi使用了openfst來(lái)讀取并存儲(chǔ)這個(gè)狀態(tài)轉(zhuǎn)移圖;而在NVIDIA實(shí)現(xiàn)的GPU版本中,依然是先用openfst讀取并存儲(chǔ)這個(gè)狀態(tài)轉(zhuǎn)移圖,但是隨后會(huì)轉(zhuǎn)換成自己實(shí)現(xiàn)的一個(gè)cudafst,用于在GPU上進(jìn)行高效的解碼。顯然,在后者的解碼過(guò)程當(dāng)中,openfst那部分所占用內(nèi)存應(yīng)該是完全不需要的,然而從htop中觀察到的內(nèi)存占用情況卻晰的表明,在代碼中顯示的使用delete xxx(xxx為openfst對(duì)象)并沒(méi)有產(chǎn)生任何效果,也就是說(shuō)相應(yīng)的內(nèi)存并沒(méi)有被釋放掉?于是我又使用valgrind再跑了一次,詭異的事情發(fā)生了,在valgrind的結(jié)果里面顯示,openfst那部分內(nèi)存是被釋放掉了,可同時(shí)在htop里面觀察到的結(jié)果依然是內(nèi)存占用居高不下,這個(gè)矛盾的結(jié)果讓我卡了很久。

折騰了很久之后,我瞎折騰把openfst對(duì)象里面的狀態(tài)轉(zhuǎn)移圖簡(jiǎn)單log了一下,于是發(fā)現(xiàn)了一個(gè)很有意思的事情:所謂的狀態(tài)轉(zhuǎn)移圖在代碼層面,是一個(gè)有長(zhǎng)度為1億多的std::vector,里面存儲(chǔ)的是對(duì)象指針,每個(gè)對(duì)象指針指向的對(duì)象又包含著一個(gè)小std::vector,每個(gè)大小不固定,徘徊在2~60之間

但是這個(gè)發(fā)現(xiàn)并沒(méi)有直接幫助我解決問(wèn)題,我在瞎折騰時(shí)嘗試了另一種方法,delete掉openfst之后,再次創(chuàng)建一個(gè)同樣的openfst對(duì)象,并觀察htop中的內(nèi)存占用情況,在這個(gè)情況下,delete之后,內(nèi)存占用沒(méi)有減少,而創(chuàng)建一個(gè)新的openfst對(duì)象,內(nèi)存幾乎沒(méi)有發(fā)現(xiàn)變化,因此這個(gè)嘗試告訴我們:內(nèi)存并非沒(méi)有釋放,而是由于某種原因沒(méi)有返還給操作系統(tǒng),依然由程序自身占用并管理著

于是,我又有針對(duì)性的在網(wǎng)上做了一番搜索,找到了解決方案。相關(guān)的資料我列在這里供大家參考:

  • https://stackoverflow.com/questions/10943907/linux-allocator-does-not-release-small-chunks-of-memory
  • https://www.cnblogs.com/lookof/archive/2013/03/26/2981768.html

簡(jiǎn)單來(lái)說(shuō),“罪魁禍?zhǔn)住闭莋libc的一個(gè)默認(rèn)機(jī)制:非常小的內(nèi)存塊在釋放時(shí)不返還給操作系統(tǒng),而由程序自己管理,下次需要使用時(shí)直接分配,無(wú)需再向操作系統(tǒng)申請(qǐng)。這個(gè)機(jī)制的出發(fā)點(diǎn)的是好的,為了提高效率嘛,畢竟調(diào)用操作系統(tǒng)API沒(méi)有那么高效,何況一個(gè)普通的程序產(chǎn)生的內(nèi)存碎片并不會(huì)太多,影響也無(wú)傷大雅。誰(shuí)知道Kaldi所使用的這個(gè)openfst產(chǎn)生的內(nèi)存碎片有1億多份,加起來(lái)總量有10多個(gè)G,而且?guī)缀跞吭陂撝狄韵?#xff0c;全都沒(méi)有釋放掉……

(ps:在Linux下,malloc()/free()的實(shí)現(xiàn)是由glibc負(fù)責(zé)的。這是一個(gè)相當(dāng)?shù)讓拥膸?kù),它會(huì)根據(jù)一定的策略,與系統(tǒng)底層通信(調(diào)用系統(tǒng)API)。因?yàn)間libc的這層關(guān)系,在涉及到內(nèi)存管理方面,用戶程序并不會(huì)直接和linux kernel進(jìn)行交互,而是交由glibc托管,所以可以認(rèn)為glibc提供了一個(gè)默認(rèn)版本的內(nèi)存管理器。它們的關(guān)系就像這樣:用戶程序 —> glibc —> linux kernel)

解決方法也非常簡(jiǎn)單,只需要在原來(lái)的代碼中加一行malloc_trim(0)(依賴malloc.h)即可,這行代碼會(huì)將剛才所有的內(nèi)存碎片返還給操作系統(tǒng)。

多線程中的陷阱

前面我們說(shuō)到還有一個(gè)問(wèn)題是隨著程序的運(yùn)行,內(nèi)存會(huì)不斷增長(zhǎng),顯然也是某個(gè)地方內(nèi)存未被釋放累積引起的,但是遺憾的是,觀察valgrind的tracking結(jié)果之后發(fā)現(xiàn),這部分內(nèi)存不是在堆上,而是在棧上。而下面的這個(gè)問(wèn)題需要我們深入到代碼層面,逐步調(diào)試。

具體的方法其實(shí)難度不大,但是對(duì)耐心要求較高……這里我所使用的方法是gdb外加watch監(jiān)視內(nèi)存變化。

# 先啟動(dòng)gdb gdb ./batched-wav-nnet3-cuda config.yaml # 打開(kāi)另外一個(gè)shell 可以利用tmux的panel,這樣在同一個(gè)界面里比較容易觀察 watch -n 0.1 -d cat /proc/[pid]/statm # statm顯示的結(jié)果的含義(注意這里的單位是頁(yè),也就是4KB,換算內(nèi)存占用的時(shí)候要乘以4) # size (1) total program size # (same as VmSize in /proc/[pid]/status) # resident (2) resident set size # (same as VmRSS in /proc/[pid]/status) # share (3) shared pages (i.e., backed by a file) # text (4) text (code) # lib (5) library (unused in Linux 2.6) # data (6) data + stack # dt (7) dirty pages (unused in Linux 2.6)# 上面的幾個(gè)結(jié)果,我們重點(diǎn)關(guān)注第二項(xiàng)resident即可

如果目標(biāo)程序是一個(gè)單線程的程序,那么這個(gè)方法可以說(shuō)是非常perfect的,哪一行代碼申請(qǐng)占用內(nèi)存,所見(jiàn)即所得。

遺憾的是,我們面對(duì)的程序是一個(gè)多線程程序,所以使用gdb的step和continue調(diào)試當(dāng)前線程的時(shí)候,其他線程也是同步執(zhí)行的;也就是說(shuō)如果你發(fā)現(xiàn)監(jiān)視窗口當(dāng)中內(nèi)存占用增加了,那并不一定是當(dāng)前代碼引起的……所以我們?cè)谡{(diào)試當(dāng)前線程時(shí),必須鎖定其他線程,如下

set scheduler-locking off|on|step # off 不鎖定任何線程,也就是所有線程都執(zhí)行,這是默認(rèn)值。 # on 只有當(dāng)前被調(diào)試程序會(huì)執(zhí)行。 # step 在單步的時(shí)候,除了next過(guò)一個(gè)函數(shù)的情況(熟悉情況的人可能知道,這其實(shí)是一個(gè)設(shè)置斷點(diǎn)然后continue的行為)以外,只有當(dāng)前線程會(huì)執(zhí)行。

但是這個(gè)方案并不完美,我們面對(duì)的這個(gè)多線程程序涉及到多個(gè)隊(duì)列,如果鎖死了其他線程,某些前置的任務(wù)沒(méi)有放進(jìn)隊(duì)列,當(dāng)前調(diào)試的線程也會(huì)卡住不動(dòng)。總之,需要來(lái)回切換,非常折磨耐心,在一番折騰之后總算是定位到了問(wèn)題所在,問(wèn)題還是出在和STL相關(guān)的地方,std::unordered_map里面存儲(chǔ)的任務(wù)狀態(tài)未被釋放引起的。

更多gdb調(diào)試的tips可以參考下面這個(gè)文章

  • https://coolshell.cn/articles/3643.html

0x02 更進(jìn)一步?

當(dāng)然,如果我們僅僅于滿足解決幾個(gè)小問(wèn)題是遠(yuǎn)遠(yuǎn)不夠的,我們希望的是掌握一套方法論,在面對(duì)同樣甚至更復(fù)雜問(wèn)題時(shí)能夠有條理和步驟的各個(gè)擊破。現(xiàn)在問(wèn)題來(lái)了,valgrind的massif工具是自己決定在程序的哪個(gè)階段做snapshot,這個(gè)粒度是非常粗線條的。而單獨(dú)使用gdb的話又不能充分利用valgrind的優(yōu)勢(shì),只能單步慢慢分析,還要面對(duì)多線程的難題,效率低下。有沒(méi)有一種方法,能夠像外科手術(shù)一樣精確,比如執(zhí)行到某行代碼時(shí)停住,然后用valgrind做snapshot,并用于后續(xù)對(duì)比分析?

這個(gè)時(shí)候如果仔細(xì)閱讀一下valgrind的說(shuō)明文檔,就會(huì)發(fā)現(xiàn)這個(gè)工具的強(qiáng)大遠(yuǎn)遠(yuǎn)超出你的想像。valgrind集成了一個(gè)vgdb工具,能夠很好的和gdb配合起來(lái),在調(diào)試程序的過(guò)程中,利用gdb步進(jìn)到需要snapshot的位置,利用內(nèi)置的命令即可snapshot,具體操方式如下所示:

# 基本命令和剛才類似,使用vgdb需要加入--vgdb=yes和--vgdb-error=0選項(xiàng) valgrind --tool=massif --pages-as-heap=yes --threshold=0.0 --time-unit=ms --vgdb=yes --vgdb-error=0 --massif-out-file=memory_footprint ./batched-wav-nnet3-cuda config.yaml # 執(zhí)行以上命令后,會(huì)啟動(dòng)vgdb,同時(shí)出現(xiàn)一行提示信息,類似 # target remote | /path/to/your/vgdb --pid=xxxxx # 這個(gè)命令等下需要用到# 啟動(dòng)另一個(gè)shell,執(zhí)行 gdb ./batched-wav-nnet-cuda # 然后再執(zhí)行剛才的命令 target remote | /path/to/your/vgdb --pid=xxxxx # 然后就可以像調(diào)試正常程序一樣下斷點(diǎn)、步進(jìn)、步過(guò)等等 # ……# 當(dāng)我們到達(dá)需要做snapshot的位置時(shí) # 在gdb的shell里面輸入 monitor detailed_snapshot [file_name] # 就可以將當(dāng)前狀態(tài)下的內(nèi)存快照保存下來(lái)

當(dāng)我們有了幾個(gè)需要重點(diǎn)關(guān)注的狀態(tài)的內(nèi)存snapshot以后,即可通過(guò)diff命令找出發(fā)生變化的地方,再進(jìn)行具體的分析。

有了這樣一套強(qiáng)大的分析工具和流程,我覺(jué)得在面對(duì)大多數(shù)內(nèi)存泄漏問(wèn)題時(shí),只要有足夠的耐心, 一定能夠準(zhǔn)確定位到有問(wèn)題的代碼部分~

總結(jié)

以上是生活随笔為你收集整理的Kaldi内存泄漏问题排查的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。