Every compilation leaks memory: the LLVM context, and on `main` several target machines
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 35/100
Direzione di ricerca
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.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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:
- Target machines.
llvm_machine(job.config.target)creates a newTargetMachinethat nobody disposes of. It's called several times per compilation (the optimization pipeline inoptimize!/emit_llvmandmcgen), which accounts for most of the ~1.3 MiB. The LLVM.jl 10 port (tb/llvm10) disposes of them (through awith_llvm_machine(target) do tm ... endhelper), so this one goes away with it. - 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_nativemove it into the native-code descriptor:data->M = std::move(clone)(src/aotcompile.cpp). A movedorc::ThreadSafeModulecarries itsThreadSafeContext, 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, itsThreadSafeModuleand therefore the context live forever. - On
main,meta.iris the module inside that descriptor (borrowed throughjl_get_llvm_module), so it's never freed either. Disposing ofmeta.irfrees the module, but not the context. - On Julia 1.14, the descriptor only keeps a pointer to our thread-safe module (
data->TSM_ref). Onmainthat one is never disposed of, so the context leaks the same way. The port disposes of it after taking the module out withLLVM.unsafe_take_module!, which is why the context is freed there on 1.14. Usingmeta.irafterJuliaContextreturns 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_nativewith 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 noparamsargument). That relies on an implementation detail, so ajl_free_native-style entry point in Julia would be better. With the port, this would free the context whenJuliaContextreturns on 1.11+. - Julia 1.10 (LLVM 15) can't move the module out of a thread-safe module, so there
meta.iris 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")
- Lingua principale
- Julia
- Stelle
- 188
- Fork
- 71
- Merge medio
- 1g 14h
- PR unite (30g)
- 65
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di JuliaGPU/GPUCompiler.jl
-
spirv
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
JuliaGPU/GPUCompiler.jl#1008 ·
I maintainer di solito rispondono entro 1 giorno
-
installation regression
Difficoltà 4/5 3-5 giorni Idoneità per principianti 52/100
JuliaGPU/GPUCompiler.jl#1019 ·
I maintainer di solito rispondono entro 1 giorno
-
gcn latency
Difficoltà 5/5 Più di una settimana Idoneità per principianti 10/100
JuliaGPU/GPUCompiler.jl#1014 ·
I maintainer di solito rispondono entro 1 giorno
-
latency
Difficoltà 5/5 Più di una settimana Idoneità per principianti 12/100
JuliaGPU/GPUCompiler.jl#1011 ·
I maintainer di solito rispondono entro 1 giorno
-
metal
Difficoltà 5/5 Più di una settimana Idoneità per principianti 28/100
JuliaGPU/GPUCompiler.jl#965 ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di JuliaGPU/GPUCompiler.jl
Issue simili
-
_fit_phylo_binomial_xlv: default start calls undefined logit (UndefVarError without beta_init)Apertacode-review
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
JuliaNLSolvers/Optim.jl#1296 ·
-
OptimizationCUTEst jobs fail: gfortran not installed on the runnerForse già presa @ChrisRackauckas-Claude l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
SciML/SciMLBenchmarks.jl#1946 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
JuliaSmoothOptimizers/NCL.jl#101 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 69/100