【问题标题】:Julia DifferentialEquations.jl speedJulia DifferentialEquations.jl 速度
【发布时间】:2017-11-26 22:50:04
【问题描述】:

我正在尝试测试 Julia ODE 求解器的速度。我在教程中使用了洛伦兹方程:

using DifferentialEquations
using Plots
function lorenz(t,u,du)
du[1] = 10.0*(u[2]-u[1])
du[2] = u[1]*(28.0-u[3]) - u[2]
du[3] = u[1]*u[2] - (8/3)*u[3]
end

u0 = [1.0;1.0;1.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz,u0,tspan)
sol = solve(prob,reltol=1e-8,abstol=1e-8,saveat=collect(0:0.01:100))

一开始加载包大约需要 25 秒,而代码在 Jupyter 笔记本的 Windows 10 四核笔记本电脑上运行了 7 秒。我知道 Julia 需要先预编译包,这就是加载时间这么长的原因吗?我发现 25 秒无法忍受。此外,当我使用不同的初始值再次运行求解器时,运行时间(~1s)要少得多,这是为什么呢?这是典型的速度吗?

【问题讨论】:

  • DifferentialEquations 是预编译的,所以 25 s 绝对不适合它。可能是Plots,但即便如此,25 秒对我来说听起来也太长了(但我在 Ubuntu 上,所以不确定 Windows 10 上的绘图是什么样的)。关于第二轮较短的运行,那是因为没有编译并且是预期的。
  • 会不会是这 25 秒都花在了加载库上(这仍然应该不会花那么长时间,但是您使用的是 Windows)?如果您更改微分方程而不仅仅是初始条件,求解器的第二次运行需要多长时间?
  • 是的,绘图需要很长时间才能加载
  • Plots 在我的工作机器上加载需要超过 10 秒,而在旧机器上加载则需要更长的时间。

标签: julia ode differentialequations.jl


【解决方案1】:

Tl;博士:

  1. Julia 包有一个预编译阶段。这有助于使所有进一步的using 调用更快,代价是第一个调用存储一些编译数据。这仅在每次包更新时触发。
  2. using 必须拉入包,这需要一点时间(取决于可以预编译多少)。
  3. 预编译不是“完整的”,所以当你第一次运行一个函数时,即使是从一个包中,它也必须编译。
  4. Julia 开发人员知道这一点,并且已经计划通过使预编译更加完整来摆脱 (2) 和 (3)。还有计划减少编译时间,我不知道具体细节。
  5. 所有 Julia 函数都专门针对给定的类型,并且每个函数都是一个单独的类型,因此 DiffEq 的内部函数专门针对您给定的每个 ODE 函数。
  6. 在大多数需要长时间计算的情况下,(5) 实际上并不重要,因为您不会经常更改函数(如果是,请考虑更改参数)。
  7. 但是 (6) 在交互式使用时确实很重要。它让人感觉不那么“流畅”。
  8. 我们可以摆脱对 ODE 函数的这种特殊化,但这不是默认设置,因为它会导致 2x-4x 的性能命中。也许将来会成为默认设置。
  9. 我们在这个问题上的预编译后计时仍然比 SciPy 的包装 Fortran 求解器等解决此类问题的时间好 20 倍。所以这都是编译时问题,而不是运行时问题。编译时间基本上是恒定的(调用相同函数的较大问题具有大致相同的编译),因此这实际上只是一个交互性问题。
  10. 我们(以及整个 Julia)在未来可以并且会在交互方面做得更好。

完整说明

这真的不是一个DifferentialEquations.jl 的东西,这只是一个Julia 包的东西。 25s 必须包括预编译时间。第一次加载 Julia 包时,它会进行预编译。然后在下一次更新之前不需要再次发生这种情况。这可能是最长的初始化时间,而且对于DifferentialEquations.jl 来说也很长,但同样只有在您每次更新包代码时才会发生这种情况。然后,每次using 的初始化成本都会很小。 DiffEq 相当大,所以初始化需要一点时间:

@time using DifferentialEquations
5.201393 seconds (4.16 M allocations: 235.883 MiB, 4.09% gc time)

然后如 cmets 中所述,您还拥有:

@time using Plots
6.499214 seconds (2.48 M allocations: 140.948 MiB, 0.74% gc time)

然后,第一次运行

function lorenz(t,u,du)
  du[1] = 10.0*(u[2]-u[1])
  du[2] = u[1]*(28.0-u[3]) - u[2]
  du[3] = u[1]*u[2] - (8/3)*u[3]
end

u0 = [1.0;1.0;1.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz,u0,tspan)
@time sol = solve(prob,reltol=1e-8,abstol=1e-8,saveat=collect(0:0.01:100))

6.993946 seconds (7.93 M allocations: 436.847 MiB, 1.47% gc time)

但是第二次和第三次:

0.010717 seconds (72.21 k allocations: 6.904 MiB)
0.011703 seconds (72.21 k allocations: 6.904 MiB)

那么这里发生了什么? Julia 第一次运行一个函数时,它会编译它。所以当你第一次运行solve 时,它会在运行时编译它的所有内部函数。所有进行的时间都将没有编译。 DifferentialEquations.jl 也专门研究函数本身,所以如果我们改变函数:

function lorenz2(t,u,du)
  du[1] = 10.0*(u[2]-u[1])
  du[2] = u[1]*(28.0-u[3]) - u[2]
  du[3] = u[1]*u[2] - (8/3)*u[3]
end

u0 = [1.0;1.0;1.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz2,u0,tspan)

我们将再次产生一些编译时间:

@time sol = 
solve(prob,reltol=1e-8,abstol=1e-8,saveat=collect(0:0.01:100))
3.690755 seconds (4.36 M allocations: 239.806 MiB, 1.47% gc time)

原来是这样,现在是为什么。这里有几件事。首先,Julia 包没有完全预编译。它们不会在会话之间保留实际方法的缓存编译版本。这是 1.x 版本列表中要做的事情,这将摆脱第一次命中,类似于仅调用 C/Fortran 包,因为它会提前大量编译 (AOT)职能。所以这很好,但现在只需注意有一个启动时间。

现在让我们谈谈更改功能。 Julia 中的每个函数都会自动专门处理其参数 (see this blog post for details)。这里的关键思想是 Julia 中的每个函数都是一个单独的具体类型。所以,由于这里的问题类型是参数化的,改变函数会触发编译。请注意,就是这种关系:您可以更改函数的参数(如果您有参数),您可以更改初始条件等,但它只是更改触发重新编译的类型。

值得吗?也许。我们希望专门为难以计算的东西快速处理。编译时间是恒定的(即你可以求解一个 6 小时的 ODE,它仍然需要几秒钟),因此计算成本高的计算不会在这里受到影响。在这里运行数千个参数和初始条件的蒙特卡罗模拟不会受到影响,因为如果您只是更改初始条件和参数的值,那么它将不会重新编译。但是,在您更改功能的地方进行交互使用确实会受到第二次左右的影响,这并不好。 Julia 开发人员对此的一个回答是,在 Julia 1.0 之后花费时间来加快编译时间,这是我不知道细节的事情,但我确信这里有一些唾手可得的成果。

我们可以摆脱它吗?是的。 DiffEq Online 不会为每个函数重新编译,因为它面向在线使用。

function lorenz3(t,u,du)
  du[1] = 10.0*(u[2]-u[1])
  du[2] = u[1]*(28.0-u[3]) - u[2]
  du[3] = u[1]*u[2] - (8/3)*u[3]
  nothing
end

u0 = [1.0;1.0;1.0]
tspan = (0.0,100.0)
f = NSODEFunction{true}(lorenz3,tspan[1],u0)
prob = ODEProblem{true}(f,u0,tspan)

@time sol = solve(prob,reltol=1e-8,abstol=1e-8,saveat=collect(0:0.01:100))

1.505591 seconds (860.21 k allocations: 38.605 MiB, 0.95% gc time)

现在我们可以更改函数而不产生编译成本:

function lorenz4(t,u,du)
  du[1] = 10.0*(u[2]-u[1])
  du[2] = u[1]*(28.0-u[3]) - u[2]
  du[3] = u[1]*u[2] - (8/3)*u[3]
  nothing
end

u0 = [1.0;1.0;1.0]
tspan = (0.0,100.0)
f = NSODEFunction{true}(lorenz4,tspan[1],u0)
prob = ODEProblem{true}(f,u0,tspan)

@time sol = 
solve(prob,reltol=1e-8,abstol=1e-8,saveat=collect(0:0.01
:100))

0.038276 seconds (242.31 k allocations: 10.797 MiB, 22.50% gc time)

并且 tada,通过将函数包装在 NSODEFunction(内部使用 FunctionWrappers.jl)中,它不再专门针对每个函数,并且您在每个 Julia 会话中点击编译时间一次(然后一旦缓存,每个包一次更新)。但请注意,这大约是 so I am not sure if it will be enabled by default 的 2x-4x 成本。我们可以在问题类型构造函数中默认实现这一点(即默认情况下没有额外的专业化,但用户可以以交互性为代价选择更快的速度)但我不确定这里有什么更好的默认值(随意用你的想法评论这个问题)。但它肯定会在 Julia 更改其关键字参数后很快被记录下来,因此“无编译”模式将成为使用它的标准方式,即使不是默认方式。

但只是为了把它放在透视图中,

import numpy as np
from scipy.integrate import odeint
y0 = [1.0,1.0,1.0]
t = np.linspace(0, 100, 10001)
def f(u,t):
    return [10.0*(u[1]-u[0]),u[0]*(28.0-u[2])-u[1],u[0]*u[1]-(8/3)*u[2]]
%timeit odeint(f,y0,t,atol=1e-8,rtol=1e-8)

1 loop, best of 3: 210 ms per loop

我们正在研究是否应该将这种交互便利性默认设置为比此处 SciPy 的默认设置快 5 倍而不是 20 倍(尽管我们的默认设置通常比 SciPy 使用的默认设置更准确,但这是另一个数据时间可以在基准中找到或只是询问)。一方面它易于使用是有意义的,但另一方面,如果重新启用长计算的专业化并且不知道蒙特卡洛(这是你真正想要速度的地方),那么那里的很多人会受到 2 到 4 倍的性能影响,这可能需要额外的几天/几周的计算。呃……艰难的选择。

因此,最终混合了优化选择和 Julia 缺少的一些预编译功能,这些功能会影响交互性而不会影响真正的运行时速度。如果您希望使用一些大的 Monte Carlo 来估计参数,或者解决大量的 SDE,或者解决一个大的 PDE,我们可以解决这个问题。那是我们的第一个目标,我们确保尽可能地达到目标。但是在 REPL 中玩耍确实有 2-3 秒的“故障”,我们也不能忽视(当然比在 C/Fortran 中玩耍要好,但对于 REPL 来说仍然不理想)。为此,我已经向您展示了已经在开发和测试的解决方案,因此希望明年的这个时候我们可以针对特定案例提供更好的答案。

PS

另外两点需要注意。如果您只使用 ODE 求解器,您可以执行 using OrdinaryDiffEq 以继续下载/安装/编译/导入所有 DifferentialEquations.jl (this is described in the manual)。此外,像这样使用saveat 可能不是解决这个问题的最快方法:用更少的点来解决它并在必要时使用密集输出可能会更好。

编辑

I opened an issue detailing how we can reduce the "between function" compilation time without losing the speedup that specializing gives。我认为这是我们可以在短期内优先考虑的事情,因为我同意我们可以在这里做得更好。

【讨论】:

  • 谢谢!这真的很有帮助。所以基本上总的运行时间就是编译时间和计算时间的总和,编译时间应该总是几秒吧?如果是这样的话,我不介意花时间在这上面。在这种特殊情况下,是因为问题非常简单,以至于编译时间比计算时间长得多,对吗?
  • 是的,就是这样。它总是几秒钟,但如果你只是一次快速地做某事,你会感觉到它。
  • 我机器上的预编译大约需要 5 到 10 分钟,之后每次加载库大约需要一分钟。我对它进行了计时,发现分配的数量比您在上面的答案中报告的要多得多。 >@time using DifferentialEquations >54.007511 seconds (49.86 M allocations: 2.870 GiB, 3.24% gc time) 我在arch linux上的设置有问题吗?
  • 自此编写以来的 3 1/2 年内编译发生了变化。而 Julia v1.6 又是一个很大的变化。到 v1.6 时,预编译时间将再次下降几乎一个数量级,但您会注意到 v1.3 中的所有内容都多预缓存了一吨,所以第一次绘图下降了一吨。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-08-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-02
  • 1970-01-01
相关资源
最近更新 更多