Skip to content

Commit c9db3d0

Browse files
authored
[METAL] Fix the rest memory leaks in Metal runtime (#8175)
* [METAL] Fix the rest memory leaks in Metal runtime When we throw exception from autoreleasepool, then the resources won't be released in proper way. In the documentation we can see that "When the block is exited with an exception, the pool is not drained.". Link on the documentation: https://clang.llvm.org/docs/AutomaticReferenceCounting.html#autoreleasepool Implemented a wrapper which handles all exceptions in autoreleasepool block and throw them after this block. * Apply comments * Add documentation comments to wrapper and macro
1 parent a74d0fe commit c9db3d0

File tree

3 files changed

+94
-28
lines changed

3 files changed

+94
-28
lines changed

src/runtime/metal/metal_common.h

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,65 @@
4242

4343
#include "../workspace_pool.h"
4444

45+
/* Macro for convenience in using AutoReleasePoolWrapper.
46+
* With this macro we can add AutoReleasePoolWrapper to our ObjC code in more
47+
* native way.
48+
*
49+
* For example, this is ObjC code with autoreleasepool:
50+
* @autoreleasepool {
51+
* // Some code
52+
* }
53+
*
54+
* To avoid possible memory leaks when an exception will be generated, we
55+
* should update this code:
56+
* AUTORELEASEPOOL { // Replace @autoreleasepool -> AUTORELEASEPOOL
57+
* // Some code
58+
* }; // Add semicolon after close bracket
59+
*
60+
* In macro AUTORELEASEPOOL we get the instance of AutoReleasePoolWrapper and
61+
* put a lambda function with code from autoreleasepool to the insertion
62+
* operator of AutoReleasePoolWrapper class.
63+
*
64+
* Note: If you want to return a value from the autoreleasepool, you should
65+
* declare the variable with result before AUTORELEASEPOOL macro. This variable
66+
* will be captured by reference and you can use it in the code in autorelease
67+
* pool. But you should write return statement after AUTORELEASEPOOL macro.
68+
*/
69+
#define AUTORELEASEPOOL tvm::runtime::metal::AutoReleasePoolWrapper::GetInstance() << [&]()
70+
4571
namespace tvm {
4672
namespace runtime {
4773
namespace metal {
74+
/*!
75+
* \brief Wrapper on autoreleasepool with exception handling
76+
*
77+
* \note In case when the exception was thrown from the autoreleasepool, the
78+
* allocated resources won't be released in proper way. So, we handle exception
79+
* in autoreleasepool and after the autoreleasepool we rethrow this exception.
80+
*/
81+
class AutoReleasePoolWrapper {
82+
public:
83+
static AutoReleasePoolWrapper& GetInstance();
84+
template <typename T>
85+
void operator<<(const T& f) {
86+
std::exception_ptr eptr;
87+
@autoreleasepool {
88+
try {
89+
f();
90+
} catch (...) {
91+
eptr = std::current_exception();
92+
}
93+
}
94+
if (eptr) std::rethrow_exception(eptr);
95+
}
96+
97+
private:
98+
AutoReleasePoolWrapper() = default;
99+
~AutoReleasePoolWrapper() = default;
100+
AutoReleasePoolWrapper(const AutoReleasePoolWrapper&) = delete;
101+
AutoReleasePoolWrapper& operator=(const AutoReleasePoolWrapper&) = delete;
102+
};
103+
48104
/*!
49105
* \brief Structure for error handling in queues
50106
*/

src/runtime/metal/metal_device_api.mm

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@
2929
namespace runtime {
3030
namespace metal {
3131

32+
AutoReleasePoolWrapper& AutoReleasePoolWrapper::GetInstance() {
33+
static AutoReleasePoolWrapper instance;
34+
return instance;
35+
}
36+
3237
MetalWorkspace* MetalWorkspace::Global() {
33-
@autoreleasepool {
34-
// NOTE: explicitly use new to avoid exit-time destruction of global state
35-
// Global state will be recycled by OS as the process exits.
36-
static MetalWorkspace* inst = new MetalWorkspace();
37-
return inst;
38-
}
38+
// NOTE: explicitly use new to avoid exit-time destruction of global state
39+
// Global state will be recycled by OS as the process exits.
40+
static MetalWorkspace* inst = new MetalWorkspace();
41+
return inst;
3942
}
4043

4144
void MetalWorkspace::GetAttr(Device dev, DeviceAttrKind kind, TVMRetValue* rv) {
42-
@autoreleasepool {
45+
AUTORELEASEPOOL {
4346
this->Init();
4447
size_t index = static_cast<size_t>(dev.device_id);
4548
if (kind == kExist) {
@@ -80,7 +83,7 @@
8083
case kDriverVersion:
8184
return;
8285
}
83-
}
86+
};
8487
}
8588

8689
static const char* kDummyKernel = R"A0B0(
@@ -161,7 +164,8 @@ int GetWarpSize(id<MTLDevice> dev) {
161164

162165
void* MetalWorkspace::AllocDataSpace(Device device, size_t nbytes, size_t alignment,
163166
DLDataType type_hint) {
164-
@autoreleasepool {
167+
id<MTLBuffer> buf;
168+
AUTORELEASEPOOL {
165169
this->Init();
166170
id<MTLDevice> dev = GetDevice(device);
167171
// GPU memory only
@@ -173,20 +177,20 @@ int GetWarpSize(id<MTLDevice> dev) {
173177
storage_mode = MTLResourceStorageModeManaged;
174178
#endif
175179
*/
176-
id<MTLBuffer> buf = [dev newBufferWithLength:nbytes options:storage_mode];
180+
buf = [dev newBufferWithLength:nbytes options:storage_mode];
177181
ICHECK(buf != nil);
178-
return (void*)(buf);
179-
}
182+
};
183+
return (void*)(buf);
180184
}
181185

182186
void MetalWorkspace::FreeDataSpace(Device dev, void* ptr) {
183-
@autoreleasepool {
187+
AUTORELEASEPOOL {
184188
// MTLBuffer PurgeableState should be set to empty before manual
185189
// release in order to prevent memory leak
186190
[(id<MTLBuffer>)ptr setPurgeableState:MTLPurgeableStateEmpty];
187191
// release the ptr.
188192
CFRelease(ptr);
189-
}
193+
};
190194
}
191195

192196
Stream* GetStream(TVMStreamHandle stream, int device_id) {
@@ -199,7 +203,7 @@ int GetWarpSize(id<MTLDevice> dev) {
199203
void MetalWorkspace::CopyDataFromTo(const void* from, size_t from_offset, void* to,
200204
size_t to_offset, size_t size, Device dev_from, Device dev_to,
201205
DLDataType type_hint, TVMStreamHandle stream) {
202-
@autoreleasepool {
206+
AUTORELEASEPOOL {
203207
this->Init();
204208
Device dev = dev_from;
205209
Stream* s = GetStream(stream, dev.device_id);
@@ -261,7 +265,7 @@ int GetWarpSize(id<MTLDevice> dev) {
261265
LOG(FATAL) << "Expect copy from/to Metal or between Metal"
262266
<< ", from=" << from_dev_type << ", to=" << to_dev_type;
263267
}
264-
}
268+
};
265269
}
266270

267271
TVMStreamHandle MetalWorkspace::CreateStream(Device dev) {
@@ -276,7 +280,7 @@ int GetWarpSize(id<MTLDevice> dev) {
276280
}
277281

278282
void MetalWorkspace::StreamSync(Device dev, TVMStreamHandle stream) {
279-
@autoreleasepool {
283+
AUTORELEASEPOOL {
280284
Stream* s = GetStream(stream, dev.device_id);
281285
// commit an empty command buffer and wait until it completes.
282286
id<MTLCommandBuffer> cb = s->GetCommandBuffer();
@@ -285,7 +289,7 @@ int GetWarpSize(id<MTLDevice> dev) {
285289
if (s->HasErrorHappened()) {
286290
LOG(FATAL) << "Error! Some problems on GPU happaned!";
287291
}
288-
}
292+
};
289293
}
290294

291295
void MetalWorkspace::SetStream(Device dev, TVMStreamHandle stream) {

src/runtime/metal/metal_module.mm

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ void Init(MetalModuleNode* m, ObjectPtr<Object> sptr, const std::string& func_na
193193
}
194194
// invoke the function with void arguments
195195
void operator()(TVMArgs args, TVMRetValue* rv, const ArgUnion64* pack_args) const {
196-
@autoreleasepool {
196+
AUTORELEASEPOOL {
197197
metal::MetalThreadEntry* t = metal::MetalThreadEntry::ThreadLocal();
198198
int device_id = t->device.device_id;
199199
auto stream = static_cast<metal::Stream*>(t->stream[device_id]);
@@ -223,7 +223,7 @@ void operator()(TVMArgs args, TVMRetValue* rv, const ArgUnion64* pack_args) cons
223223
[encoder dispatchThreadgroups:dimGrid threadsPerThreadgroup:dimBlock];
224224
[encoder endEncoding];
225225
[cb commit];
226-
}
226+
};
227227
}
228228

229229
private:
@@ -248,27 +248,33 @@ void operator()(TVMArgs args, TVMRetValue* rv, const ArgUnion64* pack_args) cons
248248

249249
PackedFunc MetalModuleNode::GetFunction(const std::string& name,
250250
const ObjectPtr<Object>& sptr_to_self) {
251-
@autoreleasepool {
251+
PackedFunc pf;
252+
AUTORELEASEPOOL {
252253
ICHECK_EQ(sptr_to_self.get(), this);
253254
ICHECK_NE(name, symbol::tvm_module_main) << "Device function do not have main";
254255
auto it = fmap_.find(name);
255-
if (it == fmap_.end()) return PackedFunc();
256+
if (it == fmap_.end()) {
257+
pf = PackedFunc();
258+
return;
259+
}
256260
const FunctionInfo& info = it->second;
257261
MetalWrappedFunc f;
258262
size_t num_buffer_args = NumBufferArgs(info.arg_types);
259263
f.Init(this, sptr_to_self, name, num_buffer_args, info.arg_types.size() - num_buffer_args,
260264
info.thread_axis_tags);
261-
return PackFuncNonBufferArg(f, info.arg_types);
262-
}
265+
pf = PackFuncNonBufferArg(f, info.arg_types);
266+
};
267+
return pf;
263268
}
264269

265270
Module MetalModuleCreate(std::string data, std::string fmt,
266271
std::unordered_map<std::string, FunctionInfo> fmap, std::string source) {
267-
@autoreleasepool {
272+
ObjectPtr<Object> n;
273+
AUTORELEASEPOOL {
268274
metal::MetalWorkspace::Global()->Init();
269-
auto n = make_object<MetalModuleNode>(data, fmt, fmap, source);
270-
return Module(n);
271-
}
275+
n = make_object<MetalModuleNode>(data, fmt, fmap, source);
276+
};
277+
return Module(n);
272278
}
273279

274280
// Load module from module.

0 commit comments

Comments
 (0)