多线程中内存无法释放

12

我一直在使用Julia进行大量数据的多线程处理,并观察到一个有趣的模式。内存使用情况(由htop报告)会慢慢增长,直到进程被操作系统杀死。该项目很复杂,很难生成一个合适的最小工作示例,但我进行了简单的实验:

using Base.Threads
f(n) = Threads.@threads for i=1:n
    x = zeros(n)
end
现在,我在我的64 Gb机器上为各种值的n(在10^4和10^5之间)重复调用f(n)。结果是有时一切都按预期工作,内存在返回后被释放,但有时情况并非如此,并且由htop报告的已使用内存数量停留在一个很大的值,即使似乎没有进行计算:

enter image description here

显式垃圾回收GC.gc()只有少量内存被释放。同时,在函数f中的循环中有时调用GC.gc()有所帮助,但问题仍然存在,并且当然会降低性能。退出Julia后,分配的内存恢复正常(可能由OS释放)。
我读过关于julia如何管理其内存以及只有当内存合计比某个值更大时才释放内存的文章。但在我的情况下,它导致进程被OS杀死。对我来说,GC似乎以某种方式失去了对所有分配的内存的跟踪。
请问有谁可以解释这种行为以及如何在不通过重复调用GC.gc()减慢代码的情况下防止它发生?为什么垃圾回收会以这种方式失效?
更多细节:
- 只有在多个线程中处理大数据(分配大量内存)时才会发生这种情况。我无法通过只使用一个线程来重现相同的问题。 - 我检查了我的代码,排除了我所知道的所有可能导致增加内存消耗的因素(全局变量、类型稳定性等),但没有积极的结果。据我所知,这些问题会导致内存分配更高,而我这里的问题是函数返回后内存没有被释放。 - 这是我的versioninfo输出:
julia> versioninfo()
Julia Version 0.7.0
Commit a4cb80f3ed (2018-08-08 06:46 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Xeon(R) Platinum 8124M CPU @ 3.00GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, skylake)
Environment:
  JULIA_NUM_THREADS = 36

2
我无法在Julia v1.1中重现这种行为(没有尝试v0.7)。如果仍然发生,那么这将是一个错误,您应该在julialang slack上询问或在GitHub上打开Julia存储库的问题。 - cmc
2个回答

1

由于此问题被问了很久,希望这种情况已经不会再发生 - 虽然没有MWE我无法确定。

然而,值得注意的一点是,Julia垃圾回收器是单线程的;即,无论您有多少个生成垃圾的线程,始终只有一个垃圾回收器。

因此,如果您要在并行工作流中生成大量垃圾,请使用多进程(即MPI.jl或Distributed.jl),而不是多线程。与多线程不同,在多进程中,每个进程都有自己的GC。


我在使用Julia和MPI.jl编写水力代码时遇到了这个问题。由于某种原因,有些时候代码可以正常运行,分配所有大数组(约400x400x400),然后内存使用量保持相对稳定,但如果我尝试重新运行,内存使用量会飙升到50GB(我的机器限制),并且导致模拟中断。这很危险,因为我需要在集群中运行我的代码,如果这种情况发生在那里,可能会引起一些人的不满。 - Luis.Alberto

0

我在使用Distributed.jl时遇到了类似的问题。同样,我尝试在每个工作进程上使用 GC.gc(),这有助于在一定程度上减少内存消耗,但对于大型任务,它最终会瘫痪机器/进程,唯一的解决方法是重新启动Julia。

我可以在Julia 1.7.1上重现您的MWE:

Julia Version 1.7.1
Commit ac5cc99908 (2021-12-22 19:35 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: AMD EPYC-Rome Processor
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-12.0.1 (ORCJIT, znver2)
Environment:
  JULIA_NUM_THREADS = 32

我还有一个使用CSV.jl的MWE,它将使用所有可用的CPU来读取大文件:
##start julia such that multiple processes are available i.e. Threads.nthreads()>1
Threads.nthreads()
using CSV,DataFrames
## memory usage in GB after starting julia and loading packages:
used_mem() = (println("$(round((Sys.total_memory()-Sys.free_memory())/2^30 -9))G used"))
used_mem()

## create large file for CSV.jl to read (you can adjust n as appropriate for your machine, this maxes out at about 7.5GB on my machine) 
n = 100000000
CSV.write("test.csv",DataFrame(repeat([(1,1,1)],n)))
used_mem()
##during the above process, memory peaks, but running garbage collection returns it to original state
GC.gc()
used_mem()

##now we load in test.csv, using all available CPUS
CSV.read("test.csv",DataFrame)
## there is now a very large dataframe in ans, so memory usage is high again
used_mem()
##clear ans and collect garbage
1+1
GC.gc()

### if Threads.nthreads()>1, memory usage is still very high, even with nothing running and no big variables to explain it
## If Threads.nthreads()=1 (i.e. start julia with `JULIA_NUM_THREADS=1`) then used_mem() is as on initialisation
varinfo()
used_mem()

我没有为Distributed.jl使用案例想出一个最小工作示例,但那是我在应用程序中遇到问题的主要地方。有趣的是,当我杀死所有工作进程时,一些内存被释放,但仍然存在一些内存占用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接