性能2-科学计算中的日志记录:调试、性能与信任

目录

科学计算中的日志记录:调试、性能与信任

你已经运行了科学模型的批处理程序,经过数小时的计算后,它输出了一个结果。然而,结果是错误的

你怀疑计算中存在错误,但不确定具体是什么问题,而缓慢的反馈循环使得调试变得更加困难。
如果能不花费数天时间运行程序就能调试并加速它,那该多好?

虽然我不是科学家,而是一名软件工程师,但我曾在科学计算领域工作了一年半。我想提供一个解决这类问题的潜在方案:日志记录,特别是我和同事们发现非常有用的一个日志库。

但在介绍解决方案之前,有必要先了解这些问题的根源:科学计算的特定特性。


科学计算的本质

从我们的角度来看,科学计算具有三个特定特性:

  1. 逻辑性: 它涉及复杂的计算。
  2. 结构性: 计算涉及处理数据并输出结果,这意味着长时间运行的批处理过程。
  3. 目标性: 科学计算的根本目标是推断现实。例如,气象学家的模型可能预测周二会下雨。

科学计算的三个问题

每个特性都伴随着相应的问题需要解决:

  1. 逻辑性: 为什么你的计算是错误的?复杂的计算使得这个问题难以确定。
  2. 结构性: 为什么你的代码很慢?对于缓慢的批处理过程,代码的缓慢比通常更加痛苦。
  3. 目标性: 你正在对现实进行推断——你真的能信任这些结果吗?

在本文的其余部分,你将看到日志记录如何帮助解决这些问题。


问题 #1:为什么你的计算是错误的?

你的批处理程序终于完成了——它只花了 12 个小时——但结果是明显错误的 ‍。

更糟糕的是,这种情况通常只在使用真实数据时发生,因此很难通过测试重现。而且,你无法在一个需要 12 小时运行的程序中使用调试器逐步调试代码!

你需要的是批处理程序运行时实际操作的记录。
也就是说,你需要记录:

  • 哪些函数调用了哪些其他函数。
  • 函数的输入和输出。
  • 中间值。

我倾向于使用 Eliot 日志库 来为科学计算添加日志记录。Eliot 的工作方式与大多数日志库非常不同,虽然它最初是为分布式系统设计的,但它也非常适合科学计算。

为了理解这一点,让我们看一个例子。

示例:一个有问题的程序

考虑以下程序:

def add(a, b):
    # ... 实现 ...
    
def multiply(a, b):
    # ... 实现 ...

def multiplysum(a, b, c):
    return multiply(add(a, b), c)

print(multiplysum(1, 2, 4))  # (1 + 2)*4 ⇒ 12

正如注释所说,我们期望输出为 12。但当我们运行它时:

$ python badmath.py
0

我们得到了 0,而不是 12——代码中有些地方出错了。

因此,我们使用 Eliot 添加了一些日志记录:

  1. 一些样板代码,告诉它将日志输出到 out.log
  2. 一个 @log_call 装饰器,记录每个函数的输入和输出。

Eliot 还有其他更复杂的 API,但我们在这里不会深入讨论。

from eliot import log_call, to_file
to_file(open("out.log", "w"))

@log_call
def add(a, b):
    # ... 实现 ...

@log_call
def multiply(a, b):
    # ... 实现 ...

# 等等。

现在我们已经添加了日志记录,我们可以运行程序,然后使用 eliot-tree 程序可视化结果:

$ python badmath.py
0
$ eliot-tree out.log
80bee1e8-7e95-43f9-a7d1-8c06fcb43334
└── multiplysum/1 ⇒ started 2019-05-01 20:54:57 ⧖ 0.001s
    ├── a: 1
    ├── b: 2
    ├── c: 4
    ├── add/2/1 ⇒ started 2019-05-01 20:54:57 ⧖ 0.000s
    │   ├── a: 1
    │   ├── b: 2
    │   └── add/2/2 ⇒ succeeded 2019-05-01 20:54:57
    │       └── result: 3
    ├── multiply/3/1 ⇒ started 2019-05-01 20:54:57 ⧖ 0.000s
    │   ├── a: 3
    │   ├── b: 4
    │   └── multiply/3/2 ⇒ succeeded 2019-05-01 20:54:57
    │       └── result: 0
    └── multiplysum/4 ⇒ succeeded 2019-05-01 20:54:57
        └── result: 0

如果你查看日志,你会看到一个操作树

  1. multiplysum() 接受三个输入(124)。
  2. 它调用 add(),参数为 12,并返回 3
  3. 然后它调用 multiply(),参数为 34,并返回 0
  4. 因此,0multiplysum() 的最终结果。

现在你可以看到问题出在哪里了:在 multiply() 函数中,因为 3×4 绝对不等于 0

简而言之,Eliot 的日志模型将日志记录为一系列操作,使你能够追踪逻辑和数据的流动。


问题 #2:为什么你的代码很慢?

你可能会遇到的第二个问题是计算速度慢——还记得那个 12 小时的批处理作业吗?通常的解决方案是性能分析,但分析器有一些局限性:

  1. 它们通常只支持单进程,不支持分布式系统。许多科学计算作业是大规模的多核计算,而不仅仅是单线程。
  2. 它们无法告诉你哪些输入是慢的。f() 在某些输入上可能很快,但在其他输入上非常慢——但分析器只会告诉你 f() 平均较慢。

Eliot 也可以在这方面提供帮助。首先,它支持跨多个进程的跟踪,包括对 Dask 并行计算框架的内置支持。

更重要的是,Eliot 可以告诉你日志中跟踪的操作的经过时间输入。

示例:double() 何时变慢?

考虑以下慢速程序:

@log_call
def main():
    A = double(13)
    B = double(0)
    C = double(4)
    return A * B * C

main()

鉴于其简单性,我们知道速度慢是由于 double(),但在更复杂的程序中,我们可以通过分析器来确认这一点。但具体是 double() 的哪个输入导致了速度变慢——是其中一个?还是全部?

我们使用 Eliot 运行程序,然后仅提取 double 操作的日志:

$ python slow.py
$ eliot-tree out.log | grep -A1 double.*started
    ├── double/2/1 ⇒ started 2019-04-24 19:17:00 ⧖ 0.0s
    │   ├── a: 13
--
    ├── double/3/1 ⇒ started 2019-04-24 19:17:00 ⧖ 10.0s
    │   ├── a: 0
--
    ├── double/4/1 ⇒ started 2019-04-24 19:17:10 ⧖ 0.0s
    │   ├── a: 4

我们可以看到,double() 对于输入 134 几乎没有花费时间——但对于输入 0,它花费了很长时间。

通过记录操作的经过时间及其输入,Eliot 可以帮助你精确定位哪些输入导致了速度变慢。


问题 #3:你能信任你的代码吗?

最后一个问题是信任——在你分享你的工具或结果之前,你需要相信它确实做了它应该做的事情。当你分享你的工具或发现时,你会希望其他人相信你的论点和结果。

一些确保信任的技术包括:

  • 可重复性: 如果我运行你的代码并得到与你不同的结果,我不会信任任何一个结果。
  • 自动化测试: 使用简单数据的单元测试,以及针对高级需求的 变形测试,可以增加对软件正确性的信心。
  • 与实际数据和其他模型的比较: 如果你正在构建一个天气模型,你可以在历史数据上运行它,看看它的表现如何,并与其他模型进行实时比较。

但即使有了所有这些技术,如果你的软件是一个不透明的黑匣子,结果将更难被信任。也许你过度拟合了历史数据,或者返回了看似合理但错误的结果。

因此,为了获得信任,你还需要提供一个关于软件操作的连贯解释:

  1. 我们做了 A——
  2. ——这里是中间结果的图表。
  3. 然后我们做了 B——
  4. ——这里是一个表格,说明为什么这是合理的。
  5. 因此,我们可以得出结论 C。
使用 Jupyter 进行解释

Jupyter 是一个擅长呈现解释的工具。你可以在代码执行之间穿插可视化和文字解释,将科学软件变成一个自我解释的叙述。

Jupyter 的问题在于它与软件工程的开发方式不太兼容。例如:

  • 为 Jupyter 笔记本编写自动化测试要困难得多,因此通常不会发生。
  • 笔记本中的软件可重用性较低,往往模块化程度较低。
使用 Eliot 进行解释

Eliot 是另一种解释软件工作原理的方式,因为它可以提供程序执行的跟踪记录,包括计算的中间结果。由于它是一个日志库,而不是像 Jupyter 这样的运行软件的方式,因此它与标准软件工程开发实践很好地集成。

缺点是,它提供的解释通常只对代码作者有意义。你不能合理地与他人分享 eliot-tree 的输出。

因此,虽然它可以帮助你增加对自己软件的信任,但它无法帮助你获得他人的信任。

在你的科学计算中使用日志记录

日志记录将帮助你:

  1. 调试代码。
  2. 加速代码。
  3. 理解并信任结果。

当然,为了获得这些好处,你需要在批处理作业开始神秘失败之前添加日志记录。所以花几分钟时间 了解更多关于 Eliot 的信息,然后为你自己的软件添加日志记录。

你可能感兴趣的:(自动化测试,python,pandas,numpy,算法)