-
Notifications
You must be signed in to change notification settings - Fork 97
Expand file tree
/
Copy pathalloc_cache.jl
More file actions
169 lines (140 loc) · 4.4 KB
/
alloc_cache.jl
File metadata and controls
169 lines (140 loc) · 4.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
using ..GPUArrays
@static if VERSION < v"1.11"
using ScopedValues
else
using Base.ScopedValues
end
mutable struct AllocCache
lock::ReentrantLock
busy::Dict{UInt64, Vector{DataRef}}
free::Dict{UInt64, Vector{DataRef}}
function AllocCache()
cache = new(
ReentrantLock(),
Dict{UInt64, Vector{Any}}(),
Dict{UInt64, Vector{Any}}()
)
return finalizer(unsafe_free!, cache)
end
end
function get_pool!(cache::AllocCache, pool::Symbol, uid::UInt64)
pool = getproperty(cache, pool)
uid_pool = get(pool, uid, nothing)
if uid_pool === nothing
uid_pool = pool[uid] = DataRef[]
end
return uid_pool
end
function cached_alloc(f, key)
cache = ALLOC_CACHE[]
if cache === nothing
return f()::DataRef
end
ref = nothing
uid = hash(key)
Base.@lock cache.lock begin
free_pool = get_pool!(cache, :free, uid)
if !isempty(free_pool)
ref = Base.@lock cache.lock pop!(free_pool)
end
end
if ref === nothing
ref = f()::DataRef
ref.cached = true
end
Base.@lock cache.lock begin
busy_pool = get_pool!(cache, :busy, uid)
push!(busy_pool, ref)
end
return ref
end
function free_busy!(cache::AllocCache)
Base.@lock cache.lock begin
for uid in keys(cache.busy)
busy_pool = get_pool!(cache, :busy, uid)
isempty(busy_pool) && continue
free_pool = get_pool!(cache, :free, uid)
append!(free_pool, busy_pool)
empty!(busy_pool)
end
end
return
end
function unsafe_free!(cache::AllocCache)
Base.@lock cache.lock begin
for pool in values(cache.busy)
isempty(pool) || error("Cannot invalidate a cache that's in active use")
end
for pool in values(cache.free), ref in pool
# release the reference
ref.cached = false
unsafe_free!(ref)
end
empty!(cache.free)
end
return
end
function Base.sizeof(cache::AllocCache)
sz = UInt64(0)
Base.@lock cache.lock begin
for kind in (cache.free, cache.busy), (_, pool) in kind
sz += sum(sizeof, pool; init = UInt64(0))
end
end
return sz
end
function Base.show(io::IO, cache::AllocCache)
sz, n_free, n_busy = Base.@lock cache.lock begin
sz = sizeof(cache)
n_free = sum(p -> length(p[2]), cache.free; init = 0)
n_busy = sum(p -> length(p[2]), cache.busy; init = 0)
sz, n_free, n_busy
end
return print(io, "AllocCache(n_free=$n_free, n_busy=$n_busy, sizeof=$(Base.format_bytes(sz)))")
end
const ALLOC_CACHE = ScopedValue{Union{Nothing, AllocCache}}(nothing)
"""
@cached cache expr
Evaluate `expr` using allocations cache `cache`.
When GPU memory is allocated during the execution of `expr`, `cache` will first be checked.
If no memory is available in the cache, a new allocation will be requested.
After the execution of `expr`, all allocations made under the scope of `@cached` will be
cached within `cache` for future use. This is useful to avoid relying on GC to free GPU
memory in time.
Once `cache` goes out scope, or when the user calls `unsafe_free!` on it, all cached
allocations will be freed.
# Example
In the following example, each iteration of the for-loop requires 8 GiB of GPU memory.
Without caching those allocations, significant pressure would be put on the GC, resulting
in high memory usage and latency. By using the allocator cache, the memory usage is stable:
```julia
cache = GPUArrays.AllocCache()
for i in 1:1000
GPUArrays.@cached cache begin
sin.(CUDA.rand(Float32, 1024^3))
end
end
# optionally: free the memory now, instead of waiting for the GC to collect `cache`
GPUArrays.unsafe_free!(cache)
```
See [`@uncached`](@ref).
"""
macro cached(cache, expr)
try_expr = :(@with $(esc(ALLOC_CACHE)) => cache $(esc(expr)))
fin_expr = :(free_busy!($(esc(cache))))
return quote
local cache = $(esc(cache))
GC.@preserve cache $(Expr(:tryfinally, try_expr, fin_expr))
end
end
"""
@uncached expr
Evaluate expression `expr` without using the allocation. This is useful to call from within
`@cached` to avoid caching some allocations, e.g., because they can be returned out of the
`@cached` scope.
"""
macro uncached(expr)
return quote
@with $(esc(ALLOC_CACHE)) => nothing $(esc(expr))
end
end