Skip to content

Commit b7fb0b0

Browse files
authored
Merge pull request #885
Improving memory requirements on AMDGPU
2 parents 7eaf7b5 + 4ba5b7b commit b7fb0b0

4 files changed

Lines changed: 335 additions & 44 deletions

File tree

src/memory.jl

Lines changed: 227 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,37 @@ function soft_memory_limit()
9797
SOFT_MEMORY_LIMIT[] = soft_limit
9898
end
9999

100+
101+
## allocation statistics
102+
103+
mutable struct AllocStats
104+
Base.@atomic alloc_count::Int
105+
Base.@atomic alloc_bytes::Int
106+
107+
Base.@atomic free_count::Int
108+
Base.@atomic free_bytes::Int
109+
110+
Base.@atomic total_time::Float64
111+
end
112+
113+
AllocStats() = AllocStats(0, 0, 0, 0, 0.0)
114+
115+
Base.copy(s::AllocStats) =
116+
AllocStats(s.alloc_count, s.alloc_bytes,
117+
s.free_count, s.free_bytes, s.total_time)
118+
119+
Base.:(-)(a::AllocStats, b::AllocStats) = (;
120+
alloc_count = a.alloc_count - b.alloc_count,
121+
alloc_bytes = a.alloc_bytes - b.alloc_bytes,
122+
free_count = a.free_count - b.free_count,
123+
free_bytes = a.free_bytes - b.free_bytes,
124+
total_time = a.total_time - b.total_time)
125+
126+
const alloc_stats = AllocStats()
127+
128+
129+
## memory accounting
130+
100131
mutable struct MemoryStats
101132
# Maximum size of the heap.
102133
# Estimated during `maybe_collect` stage.
@@ -134,7 +165,7 @@ function account!(stats::MemoryStats, bytes::Integer)
134165
Base.@atomic stats.live += bytes
135166
end
136167

137-
const EAGER_GC::Ref{Bool} = Ref{Bool}(@load_preference("eager_gc", false))
168+
const EAGER_GC::Ref{Bool} = Ref{Bool}(@load_preference("eager_gc", true))
138169

139170
function eager_gc!(flag::Bool)
140171
global EAGER_GC[] = flag
@@ -210,6 +241,169 @@ function maybe_collect(; blocking::Bool = false)
210241
return
211242
end
212243

244+
245+
## pool activity tracking
246+
247+
const POOL_STATUS = AMDGPU.LockedObject(Dict{Int, Ref{Bool}}())
248+
249+
function pool_mark(dev::HIPDevice)
250+
ps = POOL_STATUS.payload
251+
did = HIP.device_id(dev)
252+
status = get(ps, did, nothing)
253+
status === nothing && return nothing
254+
return status[]
255+
end
256+
257+
function pool_mark!(dev::HIPDevice, val::Bool)
258+
ps = POOL_STATUS.payload
259+
did = HIP.device_id(dev)
260+
box = get(ps, did, nothing)
261+
if box === nothing
262+
Base.@lock POOL_STATUS.lock begin
263+
box = get!(ps, did) do
264+
Ref{Bool}(val)
265+
end
266+
end
267+
end
268+
box[] = val
269+
return
270+
end
271+
272+
273+
## reclaim hooks
274+
275+
"""
276+
reclaim_hooks
277+
278+
A list of callables that are invoked when memory needs to be reclaimed.
279+
Downstream packages can push functions into this list to free cached resources
280+
(e.g., workspace buffers, FFT plans, etc.) when GPU memory is scarce.
281+
"""
282+
const reclaim_hooks = Any[]
283+
284+
285+
## pool cleanup
286+
287+
const _pool_cleanup_task = Ref{Task}()
288+
289+
function pool_cleanup()
290+
idle_counters = Dict{Int, Int}()
291+
while true
292+
try
293+
sleep(60)
294+
catch ex
295+
if ex isa EOFError
296+
break
297+
else
298+
rethrow()
299+
end
300+
end
301+
302+
for dev in HIP.devices()
303+
did = HIP.device_id(dev)
304+
status = pool_mark(dev)
305+
status === nothing && continue
306+
307+
if status
308+
idle_counters[did] = 0
309+
else
310+
idle_counters[did] = get(idle_counters, did, 0) + 1
311+
end
312+
pool_mark!(dev, false)
313+
314+
if get(idle_counters, did, 0) >= 5
315+
HIP.device!(dev) do
316+
reclaim()
317+
end
318+
end
319+
end
320+
end
321+
end
322+
323+
324+
## reclaim
325+
326+
"""
327+
reclaim([sz=typemax(Int)])
328+
329+
Reclaims `sz` bytes of cached memory. Use this to free GPU memory before
330+
calling into functionality that does not use the memory pool. Returns the
331+
number of bytes actually reclaimed.
332+
"""
333+
function reclaim(sz::Int=typemax(Int))
334+
dev = AMDGPU.device()
335+
for hook in reclaim_hooks
336+
hook()
337+
end
338+
HIP.device_synchronize()
339+
pool = Mem.pool_create(dev)
340+
before = HIP.reserved_memory(pool)
341+
HIP.trim(pool)
342+
after = HIP.reserved_memory(pool)
343+
return Int(before - after)
344+
end
345+
346+
347+
## pool status & queries
348+
349+
"""
350+
used_memory()
351+
352+
Returns the amount of memory from the HIP memory pool that is currently
353+
in use by the application.
354+
"""
355+
function used_memory()
356+
pool = Mem.pool_create(AMDGPU.device())
357+
Int(HIP.used_memory(pool))
358+
end
359+
360+
"""
361+
cached_memory()
362+
363+
Returns the amount of backing memory currently allocated (reserved) for the
364+
HIP memory pool.
365+
"""
366+
function cached_memory()
367+
pool = Mem.pool_create(AMDGPU.device())
368+
Int(HIP.reserved_memory(pool))
369+
end
370+
371+
"""
372+
pool_status([io=stdout])
373+
374+
Report to `io` on the memory status of the current GPU and the active memory pool.
375+
"""
376+
function pool_status(io::IO=stdout)
377+
free_bytes, total_bytes = info()
378+
used_bytes = total_bytes - free_bytes
379+
used_ratio = used_bytes / total_bytes
380+
@printf(io, "Effective GPU memory usage: %.2f%% (%s/%s)\n",
381+
100*used_ratio, Base.format_bytes(used_bytes),
382+
Base.format_bytes(total_bytes))
383+
384+
pool = Mem.pool_create(AMDGPU.device())
385+
pool_used = HIP.used_memory(pool)
386+
pool_reserved = HIP.reserved_memory(pool)
387+
@printf(io, "Memory pool usage: %s (%s reserved)\n",
388+
Base.format_bytes(pool_used),
389+
Base.format_bytes(pool_reserved))
390+
391+
hard_limit = hard_memory_limit()
392+
soft_limit = soft_memory_limit()
393+
if hard_limit != typemax(UInt64) || soft_limit != typemax(UInt64)
394+
print(io, "Memory limit: ")
395+
parts = String[]
396+
if soft_limit != typemax(UInt64)
397+
push!(parts, "soft = $(Base.format_bytes(soft_limit))")
398+
end
399+
if hard_limit != typemax(UInt64)
400+
push!(parts, "hard = $(Base.format_bytes(hard_limit))")
401+
end
402+
println(io, join(parts, ", "))
403+
end
404+
end
405+
406+
213407
# TODO handle stream capturing when we support HIP graphs
214408
mutable struct Managed{M}
215409
const mem::M
@@ -275,16 +469,41 @@ function Base.convert(::Type{Mem.AbstractAMDBuffer}, managed::Managed{M}) where
275469
end
276470

277471
function pool_alloc(::Type{B}, bytesize) where B
278-
s = AMDGPU.stream()
279-
# @info "[pool_alloc] $(Base.format_bytes(bytesize))"
280-
# display(stacktrace()); println()
281-
# println()
282-
# println()
283-
Managed(B(bytesize; stream=s); stream=s)
472+
maybe_collect()
473+
time = Base.@elapsed begin
474+
s = AMDGPU.stream()
475+
managed = Managed(B(bytesize; stream=s); stream=s)
476+
end
477+
478+
Base.@atomic alloc_stats.alloc_count += 1
479+
Base.@atomic alloc_stats.alloc_bytes += bytesize
480+
Base.@atomic alloc_stats.total_time += time
481+
482+
pool_mark!(AMDGPU.device(), true)
483+
484+
if isinteractive() && !isassigned(_pool_cleanup_task)
485+
_pool_cleanup_task[] = errormonitor(Threads.@spawn pool_cleanup())
486+
end
487+
488+
return managed
284489
end
285490

286491
function pool_free(managed::Managed{M}) where M
287-
_pool_free(managed.mem, managed.stream)
492+
sz = Int(sizeof(managed.mem))
493+
sz == 0 && return
494+
495+
try
496+
time = Base.@elapsed _pool_free(managed.mem, managed.stream)
497+
Base.@atomic alloc_stats.free_count += 1
498+
Base.@atomic alloc_stats.free_bytes += sz
499+
Base.@atomic alloc_stats.total_time += time
500+
catch ex
501+
Base.showerror_nostdio(ex,
502+
"WARNING: Error while freeing $(Base.format_bytes(sz)) of GPU memory")
503+
Base.show_backtrace(Core.stdout, catch_backtrace())
504+
Core.println()
505+
end
506+
return
288507
end
289508

290509
function _pool_free(buf, stream::HIPStream)

0 commit comments

Comments
 (0)