寫House3D渲染的時候踩過的坑
ofollow,noindex">House3D 是一個用於research的互動式3D環境. 使用者可以載入一個來自SUNCG資料集的房子的模型,然後在裡面走來走去,並獲得first-person view的圖片輸入.
我寫了House3D的渲染程式碼,過程中踩到了不少神奇的坑,坑踩的多了就覺得乾脆記下來吧.
這些坑對其他人幾乎不會有任何幫助,因為都太奇怪了..
GCC 4.8
<font face="monospace"></font><font>#include </font><font><cstdio></font> <font>class</font> A { <font>public</font>: A() {} ~A() {<font>printf</font>(<font>"~A</font><font>\n</font><font>"</font>); } }; <font>class</font> B { <font>public</font>: <font>explicit</font> B(<font>const</font> A& a) : a_{a} {} <font>const</font> A& a_; }; <font>int</font> main() { A a; B b{a}; <font>printf</font>(<font>"HERE</font><font>\n</font><font>"</font>); }
gcc 4.8 對這段程式碼會輸出:
~A HERE ~A
在B的constructor中,A居然就已經被destroy了! 你的object被編譯器偷偷刪了,怕不怕.
當時換到一臺gcc 4.8的機器,程式碼就開始出現OpenGL error. 雖然事後知道是由於相關的OpenGL handle被錯誤釋放導致無法渲染, 但OpenGL的error code資訊量極低:可以認為OpenGL只能告訴你他掛了,但不能告訴你他為什麼掛了. 所以瞬間很懵逼.
由於同一個bug, 這段程式碼在 -Wextra
下會產生一個不應產生的warning. 試了一會之後注意到這個多出來的 warning, 才發現這個bug.
已於gcc 4.9中fix: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=50025 .
TinyObjLoader
House3D 使用 tinyobjloader 讀取obj檔案. 因為要支援完整的obj格式,所以找了這個庫.看README和API感覺還挺好用的.
有一天,同事從一個新資料集(matterport3D)搞了一個obj檔案,用我的程式碼渲染出來發現慘不忍睹:

在此前我只渲染過一些簡單的模型,或人造的房子模型. 而這次的模型是真實場景掃描出來的,因此首先覺得應該是自己程式碼裡對於一些"高階的"面片沒有處理好, 花了很多時間在檢查自己的程式碼上. 檢查了一會之後無果,開始手動簡化obj檔案, 並對比我的渲染結果和meshlab的渲染結果. 當obj檔案足夠小的時候,終於意識到: tinyobjloader少讀了很多面片!
原來tinyobjloader的obj parser寫掛了, 當一些語句以特定順序出現的時候會丟失面片: https://github.com/syoyo/tinyobjloader/issues/138
解決了這個問題之後,另一個bug就很容易意識到了: OpenGL在處理紋理貼圖的時候,是以圖片左下角為(0, 0)而不是左上角, 因此我的所有貼圖的座標都錯了.而在此前簡單的模型上,紋理貼圖大多數都是對稱的, 因此沒有意識到這一點. 修好之後就很好看了:

如果這兩個bug中的任一個單獨出現,都很容易從渲染結果中猜到bug. 然而由於錯誤是兩個bug共同作用的結果, 一個 猜想是不足以合理的解釋錯誤的. 這種多個bug共同導致的錯誤往往更難debug,因為難以提出有效的猜想.
Pybind11
有一天發現House3D和pytorch一起用,會在import的時候segfault.
pytorch的符號表一直不乾不淨,跟別人一起segfault是家常便飯. gdb表示是pybind11裡的一些函式segfault的. 翻了翻pybind11的程式碼,發現是 這個bug .
pybind11需要使用一些全域性符號. 而House3D和pytorch使用了不同commit的pybind11, 存在一些二進位制不相容. 因此這些符號的名字在不同版本間應避免重名,否則一起使用就會炸.
macOS Anaconda
有一天,House3D突然不能在macOS + Anaconda上用了,python直譯器報錯:
Fatal Python error: PyThreadState_Get: no current thread Abort trap: 6
程式碼沒改過,突然就不能跑了,也是很懵逼的. 研究了半天才發現原因:
從某個版本開始,macOS上的Anaconda在python的可執行檔案裡靜態連結libpython, 而不是動態連結. 而我編譯python extension的時候動態連結了libpython. 結果一個process裡出現了兩份libpython, 導致了上面的錯誤.
一個workaround是編譯extension的時候不要link libpython, 並使用Darwin linker的 -undefined dynamic_lookup
選項. 這樣linker會忽略undefined symbol, 在之後load這個extension的時候在進行symbol lookup.
標準的編譯python extension的做法是使用 sysconfig.get_config_var('LDSHARED')
來獲取正確的linker flags, 這也是 distutils編譯so的做法 . 而我之前使用 python-config --ldflags
是錯誤的: python-config
提供的是給其他人link libpython所需要的flag, 而不是link python extension的flag.
然而,這種標準做法也有他的坑. LDSHARED
包含的flag是編譯出python的那個編譯器所使用的. 如果編譯extension需要使用一個不同版本的編譯器,這些flag未必有效.
我在ArchLinux上常遇到這樣的問題: 由於Arch的版本更新太快, LDSHARED
包含的flag是適用於gcc 7或者8的. 然而一些舊的程式碼,或一些使用cuda的程式碼不得不使用gcc 5或6編譯.此時 LDSHARED
裡的一些flag就會導致編譯錯誤. pytorch和pytorch extension就長期 難以在ArchLinux上編譯 .
CSV parser
組裡有一些基於House3D的大規模實驗,單機同時渲染上百個房子. 然而經常在跑了幾個小時之後出現一個 第三方csv parser 裡的assertion error. 這種幾個小時才能隨機重現一次的一般都是記憶體問題.
首先試圖搞一個minimal reproducible example. bug是別人發現的,然而能出現bug的程式碼很複雜. 跟很多記憶體問題一樣, 一旦做一些小的程式碼簡化,就會大大降低bug重現的概率. 最後也沒能做出什麼有意義的簡化. bug都是high load跑幾小時才出現的,因此valgrind是不可能的了,asan也沒給任何有用的資訊.
起初假設是別的地方的memory corruption影響到了csv parser (畢竟一個csv parser還能寫錯?). 於是開始強行看這個csv parser的程式碼.畢竟有個assertion error,追溯起來不是特別困難. 結果發現這個parser本身還真的有bug.
這個bug 也是很難觸發了.根本原因是一個1 byte的uninitialized read. 當且僅當csv檔案末尾字元不是"\n", 且這個 uninitialized read恰好讀到一個"\n" 的時候,才會觸發崩潰. 也難怪用簡單的程式碼根本無法reproduce了, 必須要先把free memory搞得足夠亂才有機會觸發.
深深的覺得github上的code真不能隨便拿來include.
EGL under cgroup
House3D可以使用 EGL 進行渲染,nvidia的顯示卡對EGL有著不錯的支援: 使用EGL可以在沒有X server的時候渲染,還能支援多卡並行渲染,增大throughput. high throughput對一些data hungry的RL演算法是很有必要的.
當實驗規模大起來,使用叢集之後,發現EGL經常渲染第一張圖就崩潰. 分析發現所有8卡的任務都沒崩潰,因此猜對了問題的方向.
像大多數job scheduling system一樣,我們提交的訓練任務會在一個cgroup裡執行. cgroup會給任務分配資源,包括可使用的CPU,GPU,記憶體.
cgroup如何限制對GPU的使用?方法很簡單粗暴:設定了cgroup內部對於 /dev/nvidiaX
的訪問許可權. 在cgroup內 strace -fe file nvidia-smi
可以看到, nvidia-smi
會試圖訪問所有 /dev/nvidiaX
, 最後會忽略沒有許可權的裝置,列出可以訪問的裝置. cuda的device id也會被對映到有許可權的裝置上.
然而EGL並不是這樣!即使cgroup限制了GPU的使用, eglQueryDevicesEXT
函式仍然能夠返回物理機上所有的GPU. EGL會以為他可以使用所有的GPU, 但在真正使用時會segfault.
這應該算是nvidia的bug. 最終我自己加了一層wrapper, 檢查了一下許可權.
EGL Multithreading
這確實是nvidia的bug了. 有人報告說在一臺機器上,好好的程式碼會崩潰.
gdb顯示崩在libEGL裡. 一臉懵逼的調了半天啥也沒試出來,只是發現多執行緒的時候才會崩潰.
不知道抱著什麼心態翻了翻nvidia驅動的release notes, 居然就在 某個版本的下載頁面 裡看到了這個bug:
Fixed a bug that could cause multi-threaded EGL applications to Crash when exiting.
不知道該不該高興.
EGL resource leak
應該還是nvidia的bug. 至今沒修.
有人發現,開了太多EGL context之後就不能再開了. 猜想是有resource leak. 折騰了一會搞出了 reproducible example .
這個leak有意思在於: 好好的開了context再destroy,是不會leak的. 但是如果destroy的時候還有其他context存活,則資源不會被釋放. 只有當不存在存活的context時,每個context對應的資源才會被釋放.
反應在python中, 這段程式碼:
for k in range(1000): ctx = create_EGL_context()
會resource leak, 然而這段等價的程式碼則沒有問題.
for k in range(1000): ctx = None ctx = create_EGL_context()
也是很坑了.