Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

Every compilation leaks memory: the LLVM context, and on `main` several target machines

未关闭
#970 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

@maleadt 已经在做这个了。

开始于 2026年10月19日。

  • #1039 来自 @maleadt —— 未关闭

评估

难度
5/5
预计耗时
一周以上
新手友好度
35/100
Issue 类型
缺陷
描述清晰度
需要澄清
活跃度
活跃
技术栈
julia
领域
compilers

调研方向

Start with the reproducer and trace llvm_machine calls in optimize!, emit_llvm, and mcgen, then inspect the native-code descriptor handling in src/aotcompile.cpp, especially jl_dump_native and the ThreadSafeModule ownership paths. Done means repeated compilation no longer retains target machines, descriptors, modules, or LLVM contexts across the supported Julia versions, with the reproducer confirming the native-memory growth is gone.

由索引模型根据 Issue 内容生成。

描述

Compiling a kernel leaks native memory on every call, and JuliaContext() do ... end doesn't help: its thread-safe context is reference-counted, and the code generator's native-code object holds a second reference to it. Since that object is never freed (#756), the context and everything in it stay alive. I measured this while porting GPUCompiler to LLVM.jl 10.

Measurements

The loop compiles the same trivial kernel (kernel(x) = x + 1, NativeCompilerTarget, kernel=false) inside a fresh JuliaContext() do ... end each time, and reports RSS growth after GC.gc(true) (the Julia heap stays flat, so the growth is native memory). The script is attached at the end.

per compilation
main (LLVM.jl 9.13.2), Julia 1.12, compile(:asm) ~1380 KiB
same, after disposing of meta.ir ~1375 KiB
same, after also freeing the LLVM context by hand (LLVMContextDispose) ~1285 KiB
main, Julia 1.12, compile(:llvm) ~565 KiB
same, after freeing the context by hand ~470 KiB
LLVM.jl 10 port (tb/llvm10), Julia 1.12, compile(:asm) ~36 KiB
same, after freeing the context by hand 0
port, Julia 1.10 / 1.13, compile(:asm) ~91 / ~86 KiB
same, after freeing the context by hand ~23 / ~16 KiB

That freeing the context "by hand" after JuliaContext returned doesn't crash already shows that the context wasn't freed when JuliaContext disposed of its ThreadSafeContext.

So there are two leaks on main:

  1. Target machines. llvm_machine(job.config.target) creates a new TargetMachine that nobody disposes of. It's called several times per compilation (the optimization pipeline in optimize!/emit_llvm and mcgen), which accounts for most of the ~1.3 MiB. The LLVM.jl 10 port (tb/llvm10) disposes of them (through a with_llvm_machine(target) do tm ... end helper), so this one goes away with it.
  2. The context, which is what remains on the port: about 35–70 KiB per compilation for a trivial kernel, more for bigger ones (it holds all types, constants and metadata, and any module that wasn't disposed of).
Why the context survives

JuliaContext() creates a fresh ThreadSafeContext for every compilation, which is a shared_ptr to the LLVMContext. Disposing of it only drops our reference. In compile_method_instance, we pass a thread-safe module in that context to the code generator:

  • Up to Julia 1.13, jl_emit_native/jl_create_native move it into the native-code descriptor: data->M = std::move(clone) (src/aotcompile.cpp). A moved orc::ThreadSafeModule carries its ThreadSafeContext, i.e. another reference to the context.
  • That descriptor is only freed by jl_dump_native (delete data), which we never call (#756). So the descriptor, its ThreadSafeModule and therefore the context live forever.
  • On main, meta.ir is the module inside that descriptor (borrowed through jl_get_llvm_module), so it's never freed either. Disposing of meta.ir frees the module, but not the context.
  • On Julia 1.14, the descriptor only keeps a pointer to our thread-safe module (data->TSM_ref). On main that one is never disposed of, so the context leaks the same way. The port disposes of it after taking the module out with LLVM.unsafe_take_module!, which is why the context is freed there on 1.14. Using meta.ir after JuliaContext returns is a use-after-free on 1.14 (it segfaults); before, it worked because of the leak.

The port changes how the module is handed out, but not the context leak on ≤ 1.13: it takes the module out of the descriptor with LLVM.unsafe_take_module! (LLVM 16+), so callers own meta.ir, but the now-empty ThreadSafeModule in the descriptor still references the context.

Possible fixes
  • Free the descriptor once we're done querying it (the global variable tables, function IDs, ...). There's no dedicated API, but jl_dump_native with no outputs deletes it (if (!bc_fname && !unopt_bc_fname && !obj_fname && !asm_fname) { delete data; return; }, present on 1.10–1.14; its signature differs: 1.10/1.11 have no params argument). That relies on an implementation detail, so a jl_free_native-style entry point in Julia would be better. With the port, this would free the context when JuliaContext returns on 1.11+.
  • Julia 1.10 (LLVM 15) can't move the module out of a thread-safe module, so there meta.ir is the descriptor's module, and freeing the descriptor would free it. That needs either a copy of the module or keeping the leak on 1.10.
  • Alternatively, empty the descriptor's ThreadSafeModule (assign it a default-constructed one) to drop its context reference, which would need a small LLVMExtra function.

For now, the port keeps meta.ir as is: callers get the module and may dispose of it, and the IR must not be used after the JuliaContext it was compiled in.

Reproducer
# usage: julia --project leak.jl <drop|dispose|killctx> <N> <llvm|asm>
using GPUCompiler, LLVM
module TestRuntime
    signal_exception() = return
    malloc(sz) = C_NULL
    report_oom(sz) = return
    report_exception(ex) = return
    report_exception_name(ex) = return
    report_exception_frame(idx, func, file, line) = return
end
struct TestParams <: GPUCompiler.AbstractCompilerParams end
GPUCompiler.runtime_module(::GPUCompiler.CompilerJob{<:Any,TestParams}) = TestRuntime
kernel(x) = x + 1
function mkjob()
    source = GPUCompiler.methodinstance(typeof(kernel), Tuple{Int})
    config = GPUCompiler.CompilerConfig(GPUCompiler.NativeCompilerTarget(), TestParams(); kernel=false)
    GPUCompiler.CompilerJob(source, config)
end
rss() = parse(Int, split(read("/proc/self/statm", String))[2]) * 4096 / 2^20
variant = ARGS[1]; N = parse(Int, ARGS[2]); stage = Symbol(get(ARGS, 3, "asm"))
function once()
    ref = JuliaContext() do ctx
        asm, meta = GPUCompiler.compile(stage, mkjob())
        variant in ("dispose", "killctx") && LLVM.dispose(stage == :llvm ? asm : meta.ir)
        ctx.ref
    end
    # experiment: free the LLVM context behind the ThreadSafeContext's back
    variant == "killctx" && LLVM.API.LLVMContextDispose(ref)
end
for i in 1:50; once(); end; GC.gc(true)
r0 = rss()
for i in 1:N; once(); end; GC.gc(true)
r1 = rss()
println("$variant $stage: $(round((r1-r0)*1024/N; digits=1)) KiB per compilation")
主要语言
Julia
星标
188
派生
71
平均合并
1 天 13 小时
30 天内合并 PR
68

环境准备

这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

JuliaGPU/GPUCompiler.jl 的其他 Issue

查看 JuliaGPU/GPUCompiler.jl 的全部 Issue

相似的 Issue

更多 Julia Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。