写House3D渲染的时候踩过的坑

House3D 是一个用于research的交互式3D环境. 用户可以载入一个来自SUNCG数据集的房子的模型,然后在里面走来走去,并获得first-person view的图片输入.

我写了House3D的渲染代码,过程中踩到了不少神奇的坑,坑踩的多了就觉得干脆记下来吧.

这些坑对其他人几乎不会有任何帮助,因为都太奇怪了..

GCC 4.8

#include <cstdio>
class A {
  public:
    A() {}
    ~A() {printf("~A\n"); }
};

class B {
  public:
    explicit B(const A& a) : a_{a} {}
    const A& a_;
};

int main() {
  A a; B b{a};
  printf("HERE\n");
}

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()

也是很坑了.

Comments