谈谈Github上如何交流(2): 如何科学的报bug

报告错误 / 报 bug 是用户与开发者间最常见的一类交流, 也是常见的 github issue. 但是很多用户并不会科学的报 bug, maintainer 对此也缺乏引导. 因此这篇文章讨论如何科学的报 bug.

如何报 bug, 不仅适用于开源社区, 也适用于任何软件开发. 上一篇提到, 开源社区的交流难度比一般的团队合作更大. 如果掌握了在开源社区中报 bug / 修 bug 的交流方式, 在公司里处理类似的事情也会更轻松.

Unexpected Issues

首先, "报 bug" 是一个较为狭义的说法.

在有的项目里, 用户容易确定一个问题是不是 "bug". 但在有些项目里, 用户未必有能力判断问题到底是不是由于项目的 bug 产生的. 程序的错误可能来自于用户自己, 用户的环境, 或其他依赖.

这时候, 报告 "unexpected issues" 是个更合适的说法: 用户报告的是未预期的行为 (unexpected observations/behaviors, 不一定是 error), 然后由更了解情况的人判断它们是不是 bug.

What is Expected/Unexpected

要报告 unexpected issue, 用户应首先一定 确保对方明白自己的 expectation.

  • Expectation 有时候是很显然的, 比如 expect 程序正常运行但是它崩溃了. 然而, 很多时候, expectation 也许对问题的报告者显然, 对别人却未必.

    • 例如: 一个常见情况是用户写了一大段文字描述自己做了什么, 程序做了什么输出了什么, 看完根本不明白到底哪里是 unexpected. 通过反复询问才了解到, 用户的 expectation 是 "程序不输出 XXX". 这样的 expectation, 未必那么显然.

      人类语言往往是模糊的. 要确保对方明白你的 expectation, 以 "我 expect ..." 为开头造句最清楚. 上面的例子里, 如果用户能在流水帐的信息之外, 清楚的说出 "我 expect ...", 则避免了低效的交流.

  • 因为用户的误解, expectation 本身可能是 错误的, 没有根据的, 或不被支持的. 例如:

    • 用户: "我 expect 这个 API 输出这样的格式". 维护者: "请看文档, 它输出的是另外的格式".
    • 用户: "我 expect 方法 A 比 B 快". 维护者: "这个 expectation 没有根据, A 和 B 时快时慢, 不好说".
    • 用户: "我 expect 训练我这个模型不炸". 维护者: "想法很好, 下次不要问了. 我们不负责这个".

    由误解产生的 expectation 可能就更不显然了. 只有清楚的说出来才能尽早澄清这类误解.

要说清楚 expectation, 一般要包含两个部分:

  • 做了什么: 运行了什么命令, 写了什么代码, 点了什么按钮, 等等.
  • 期待看到什么 现象: 期待程序不崩溃, 期待程序输出特定内容, 等等.

Describe Observations, Not Presumed Behaviors

用户应描述自己看到了什么 现象 (observations) , 而不 (仅) 是自己以为程序做了什么 (presumed behaviors). 因为用户未必理解程序到底做了什么, 也未必有能力描述好程序的行为.

作为一个用户, 你 expect 程序做 X, 但是程序好像没做 X / 做了 Y, 因此你想报告 unexpected issue. 这时候, 不要下结论说程序做了 / 没做什么, 因为:

  1. 这个判断可能是错误的. 程序可能已经做了 X, 或者程序做了 Z (而不是 Y). 声称程序做了什么可能会误导别人.
  2. 你的描述可能是模糊, 不好理解的. 想象一个不懂电脑的人问你 "电脑打不开了怎么办", “不能上网怎么办 "--- 你的第一反应肯定是" 什么叫打不开 / 不能上网?". 当你描述一个自己不太了解的程序的行为的时候, 在别人眼里可能也是类似的.

如果你觉得程序做了错误的事情, 当然可以提供自己的判断和分析, 但最需要提供的是能够支持你的判断的 observations, 例如原始的 logs (如果 observation 与图片有关, 截图).

相比描述 "behavior" 来说, 提供 observation 有这些好处:

  1. 更简单: 你只要复制粘贴. 不需要了解这个程序

  2. 无歧义: 复制粘贴可以更完整的还原你的 observation, 避免了人类语言的歧义性.

  3. 提供 完整的 observations 的话, 其他人就可以跳过用户的判断, 独立判断 到底发生了什么. 这对分析 unexpected issue 是至关重要的. 用户自己的判断可能是错的, 举几个例子:

    • 用户判断程序跑的慢, 这时候用户应该提供自己跑 benchmark 的代码 / 工具, 和它们的输出. 真实情况也许是, benchmark 的方式不对, 或测量的单位变了.
      • 在 deep learning 里太常见了: 正确的 benchmark 并不容易做; 测量单位在有的系统里会随着 batch size 变化.
    • 因为 log 里有 error X, 用户判断程序由于 X 崩溃了. 但是可能 log 里另外的 Y 才是崩溃的 root cause. 用户应该提供完整的 log, 让别人独立做出判断.
    • 用户打开feature_A=True 之后触发了 failure X, 因此判断feature_A 导致了 X. 但事实可能是, feature_A=False 也会触发 failure X, 只是由于其他原因 X 没有暴露出来.

    与此相对的, maintainer 不要过度相信用户声称的 behavior. 应该从用户提供的信息中判断用户声称的 unexpected behavior 是否真的发生了.

我一般都会在 issue template 里要求用户提供 完整的 log . 这是性价比最高的信息: 不仅能够用来判断程序的行为, 还能够帮助 debug, 用户也很容易提供. 但还是总有人在报告 error 的时候只给一行 error message, 连 stack trace 都没有, 让人很头疼. 希望未来的 github issue form 能够通过强制必填的表单来更好的教育用户.

重要的事情再说一遍: maintainer 需要 全部的, 完整的 log, 而不仅仅是 error 发生前的 log. 在用户看来没有用的信息对 maintainer 可能是有用的, 不要省略它们.

另外, 既然在报告 unexpected issue, 用户提供的 observation 当然应该清楚的包含 "unexpected" 的部分. 用户需要让 maintainer 能够从 observations 中看到这个 unexpected issue 确实发生了.

Minimal Reproducible Example (MRE)

Stackoverflow 的 "How to ask a good question" 里有提到 "Minimal Reproducible Example (MRE)" 的概念, 建议阅读.

在开源社区的场景下, 报告一个 unexpected issue 的时候, 用户也应该尽量以代码, 命令, 数据的形式提供 minimal reproducible example. 其意义在于:

  1. 帮助判断 issue 是不是 "项目的问题", 因此使这个 issue 对项目有 "contribution".
    • Reproducible, 或 verifiable, 意思是别人能够复现这个问题.
    • Minimal 的意思是, 用来复现这个问题的代码 / 数据特别少. 因此很容易判断是用户自己用错了, 还是项目错了.
  2. 帮助 maintainer debug, 研究 issue 的解决方案.

反过来:

  • 如果一个 issue 不 reproducible 的话, maintainer 很难相信这个问题存在, 或即使存在也很难去 debug.
  • 如果用户提供的 reproducible example 过于复杂的话, maintainer 不愿意也没有义务花时间理解用户的代码, 更不愿意帮着找用户自己的 bug.

为了提供一个高质量的 MRE:

  • 用户应该问自己: 别人按照我提供的步骤能够独立的 reproduce 这个 issue 吗? 有没有漏什么关键的步骤, 数据? 能不能把我的私有数据换成公开数据或者 fake/mock 数据?
    • Maintainer 也应该为项目的不同模块提供样例输入数据
  • 用户如果愿意配合, "reproducible" 大部分时候可以满足.
    • 除了那种本身需要大量时间 / 计算资源才能复现, 或随机出现的 issue -- 那样的难题没什么好的办法, 要依赖用户自己做大部分的 debug 工作. Fight Against Silent Bugs in Deep Learning Libraries就记录了我怎么 debug TensorFlow NCCL 里的一个随机出现的计算错误.
  • 而 "minimal" 则会需要用户投入一定的时间, 因为用户发现问题时, 也许自己的程序代码太复杂, 并不 minimal. 为了达到 "minimal", Stackoverflow 提供了两个有用的建议, 一般需要交替使用:
    1. Start from scratch: 如果对问题的触发条件有了猜想, 可以从头写一个简单版本看看是否能触发 issue
    2. Divide and conquer: 如果对触发条件没什么头绪, 可以开始删代码 / bisection / 简化无关的部分, 直到 issue 消失
  • 用户发表前, 问问自己: 这个 example 里还有哪里可以删掉?
    • 剩下的代码越少, 对 maintainer 的帮助就越大
    • 删掉看似无关的代码之后, 务必再确认一下 issue 仍然可以 reproduce
    • 不要省略有用的代码, 例如 python 里的 import: maintainer 为了复现还得手动把它们加回来呀. 而且 import 也有 side effect, 可能导致 bug, 例如这个
  • 项目本身的良好设计也能帮用户提供 MRE.
    • 如果 library 有非常清楚的接口, 没有什么内部状态, 那么用户只要把提供给 library 的输入输出记下来, 就能够复现问题
    • 反过来, 如果项目是一个 "framework", 提供了很多复杂的 semantics, 就很难简化 issue

Environment Information

用户应提供 maintainer 要求的环境信息 (项目的 version, 依赖的 version, 系统软硬件等等). 它的重要性在于:

  • 决定了 expectation: 程序在不同环境下的 expected behavior 可能是不同的
  • 有助于 reproducibility: issue 可能只在特定环境下能够 reproduce

Maintainer 最清楚哪些环境信息是需要的, 因此 maintainer 应当以 issue template 等形式告知用户如何提供环境信息. 例如, 在 detectron2 中我提供了一个collect_env.py 脚本, 运行后会输出如下的结果, 比用户自己能想到的信息要详细得多.

python collect_env.py
----------------------  -----------------------------------------------------------
sys.platform linux
Python 3.10.1 (main, Dec 18 2021, 23:53:45) [GCC 11.1.0]
numpy 1.21.5
detectron2 0.6 @/home/xxx/xxx/detectron2/detectron2
Compiler GCC 11.1
CUDA compiler CUDA 11.5
detectron2 arch flags 6.1
DETECTRON2_ENV_MODULE <not set>
PyTorch 1.10.1 @/usr/lib/python3.10/site-packages/torch
PyTorch debug build False
GPU available Yes
GPU 0 NVIDIA GeForce GTX 1070 (arch=6.1)
Driver version 495.46
CUDA_HOME /opt/cuda
Pillow 8.4.0
torchvision 0.11.0a0+7947fc8 @/home/xxx/xxx/torchvision/torchvision
torchvision arch flags 6.1
fvcore 0.1.5.post20211023
iopath 0.1.9
cv2 4.5.5
---------------------- -----------------------------------------------------------
PyTorch built with:
- GCC 11.1
- C++ Version: 201402
- Intel(R) Math Kernel Library Version 2020.0.4 Product Build 20200917 for Intel(R) 64 architecture applications
- Intel(R) MKL-DNN v2.2.3 (Git Hash 7336ca9f055cf1bfa13efb658fe15dc9b41f0740)
- OpenMP 201511 (a.k.a. OpenMP 4.5)
- LAPACK is enabled (usually provided by MKL)
- NNPACK is enabled
- CPU capability usage: AVX2
- CUDA Runtime 11.5
- NVCC architecture flags: -gencode;arch=compute_52,code=sm_52;-gencode;arch=compute_60,code=sm_60;-gencode;arch=compute_62,code=sm_62;-gencode;arch=compute_70,code=sm_70;-gencode;arch=compute_72,code=sm_72;-gencode;arch=compute_75,code=sm_75;-gencode;arch=compute_80,code=sm_80;-gencode;arch=compute_86,code=sm_86;-gencode;arch=compute_86,code=compute_86
- CuDNN 8.3
- Magma 2.6.1
- Build settings: BLAS_INFO=mkl, BUILD_TYPE=Release, CUDA_VERSION=11.5, CUDNN_VERSION=8.3.0, CXX_COMPILER=/usr/bin/c++, CXX_FLAGS=-march=x86-64 -mtune=generic -O2 -pipe -fno-plt -fexceptions -Wp,-D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fstack-clash-protection -fcf-protection -Wp,-D_GLIBCXX_ASSERTIONS -fvisibility-inlines-hidden -DUSE_PTHREADPOOL -fopenmp -DNDEBUG -DUSE_KINETO -DUSE_FBGEMM -DUSE_QNNPACK -DUSE_PYTORCH_QNNPACK -DUSE_XNNPACK -DSYMBOLICATE_MOBILE_DEBUG_HANDLE -DEDGE_PROFILER_USE_KINETO -O2 -fPIC -Wno-narrowing -Wall -Wextra -Werror=return-type -Wno-missing-field-initializers -Wno-type-limits -Wno-array-bounds -Wno-unknown-pragmas -Wno-sign-compare -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -Wno-unused-result -Wno-unused-local-typedefs -Wno-strict-overflow -Wno-strict-aliasing -Wno-error=deprecated-declarations -Wno-stringop-overflow -Wno-psabi -Wno-error=pedantic -Wno-error=redundant-decls -Wno-error=old-style-cast -fdiagnostics-color=always -faligned-new -Wno-unused-but-set-variable -Wno-maybe-uninitialized -fno-math-errno -fno-trapping-math -Werror=format -Werror=cast-function-type -Wno-stringop-overflow, LAPACK_INFO=mkl, PERF_WITH_AVX=1, PERF_WITH_AVX2=1, PERF_WITH_AVX512=1, TORCH_VERSION=1.10.1, USE_CUDA=1, USE_CUDNN=1, USE_EXCEPTION_PTR=1, USE_GFLAGS=ON, USE_GLOG=ON, USE_MKL=ON, USE_MKLDNN=ON, USE_MPI=OFF, USE_NCCL=ON, USE_NNPACK=ON, USE_OPENMP=ON,

Maintainer 实现这样的脚本时, 需要注意:

  • 最好允许它可以独立执行, 不依赖项目是否成功安装
  • Python 的包管理十分混乱, 应了解我的这篇文章里的注意事项. 例如, PyTorch 的collect_env.py 里使用{conda,pip} list 就是不科学的做法.
  • 多多捕获异常: 用户的环境里可能有各种错误, 不要假设所有信息都能被 collect 到.

有时候, 用户仅仅提供自己的环境信息还不足以复现问题, 因为难以确定是环境中的哪个因素导致了 issue. 为了保证 issue 的 reproducibility, 可以考虑使用 docker 或 Colab notebook 提供更完整的环境. 这种情况并不少见: 我在 PyTorch 里有 4 个 bug report 是自带 docker 来 reproduce 的. Maintainer 也应提供官方的 docker/Colab, 方便用户在报 issue 时排除环境问题: 用户可以把自己的 MRE 在官方的环境中测试.

Summary

这篇文章更多从用户的角度说了如何报告 unexpected issues. 用户最好应提供:

  1. Expectation
  2. Unexpected observations / Full observations
  3. Environment
  4. Minimal reproducible example

在 maintainer 给予了足够的引导的情况下, 1-3 的代价都很小, 用户应尽可能提供. 4 有时会有一定难度, 文中已介绍.

第一篇文章中说到, maintainer 自己决定自己的义务 / commitment 有哪些, 那么也就可以要求 unexpected issue 必须包含特定信息, 并决定对于缺少信息的 issue 不予处理. 一个很有趣的极端例子是, you-get 项目直接禁用了 issue 功能, 要求所有的 bug report 必须以 "失败的单元测试" 的 PR 形式报告, 直接满足了以上四点. 对于这种接口简单的工具来说, 不失为一个好办法.

大多数具备规模的项目会通过 issue 类别和 issue template 表明什么样的 issue 是 maintainer 愿意支持的. 为了高效管理, 往往都会对用户提供的信息有硬性要求. 如果项目有 issue template, 而你又没有自信到觉得自己提供的信息比 template 更好, 那么请务必 follow issue template -- 要获得 maintainer 的帮助, 应该首先尊重 maintainer 的要求, 提供必要的信息. 下一篇文章会更详细的说 maintainer 的管理方式.

Comments