Skip to content

Commit 69cb877

Browse files
mdouzefacebook-github-bot
authored andcommitted
Fix memory leak for ParameterSpace objects (#3007)
Summary: Pull Request resolved: #3007 There is a complicated interaction between SWIG and the python wrappers where the ownership of ParameterSpace arguments was stolen from Python. This diff adds a test, fixes that behavior and fixes the referenced_objects construction Reviewed By: mlomeli1 Differential Revision: D48404252 fbshipit-source-id: 8afa9e6c15d11451c27864223e33ed1187817224
1 parent e3731f7 commit 69cb877

File tree

2 files changed

+50
-2
lines changed

2 files changed

+50
-2
lines changed

faiss/python/class_wrappers.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,28 @@ def add_to_referenced_objects(self, ref):
10661066
else:
10671067
self.referenced_objects.append(ref)
10681068

1069+
class RememberSwigOwnership(object):
1070+
"""
1071+
SWIG's seattr transfers ownership of SWIG wrapped objects to the class
1072+
(btw this seems to contradict https://www.swig.org/Doc1.3/Python.html#Python_nn22
1073+
31.4.2)
1074+
This interferes with how we manage ownership: with the referenced_objects
1075+
table. Therefore, we reset the thisown field in this context manager.
1076+
"""
1077+
1078+
def __init__(self, obj):
1079+
self.obj = obj
1080+
1081+
def __enter__(self):
1082+
if hasattr(self.obj, "thisown"):
1083+
self.old_thisown = self.obj.thisown
1084+
else:
1085+
self.old_thisown = None
1086+
1087+
def __exit__(self, *ignored):
1088+
if self.old_thisown is not None:
1089+
self.obj.thisown = self.old_thisown
1090+
10691091

10701092
def handle_SearchParameters(the_class):
10711093
""" this wrapper is to enable initializations of the form
@@ -1080,8 +1102,9 @@ def replacement_init(self, **args):
10801102
self.original_init()
10811103
for k, v in args.items():
10821104
assert hasattr(self, k)
1083-
setattr(self, k, v)
1084-
if inspect.isclass(v):
1105+
with RememberSwigOwnership(v):
1106+
setattr(self, k, v)
1107+
if type(v) not in (int, float, bool, str):
10851108
add_to_referenced_objects(self, v)
10861109

10871110
the_class.__init__ = replacement_init

tests/test_search_params.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import faiss
99
import unittest
10+
import sys
11+
import gc
1012

1113
from faiss.contrib import datasets
1214
from faiss.contrib.evaluation import sort_range_res_2, check_ref_range_results
@@ -346,6 +348,28 @@ def test_max_codes(self):
346348
if stats.ndis < target_ndis:
347349
np.testing.assert_equal(I0[q], Iq[0])
348350

351+
def test_ownership(self):
352+
# see https://github.com/facebookresearch/faiss/issues/2996
353+
subset = np.arange(0, 50)
354+
sel = faiss.IDSelectorBatch(subset)
355+
self.assertTrue(sel.this.own())
356+
params = faiss.SearchParameters(sel=sel)
357+
self.assertTrue(sel.this.own()) # otherwise mem leak!
358+
# this is a somewhat fragile test because it assumes the
359+
# gc decreases refcounts immediately.
360+
prev_count = sys.getrefcount(sel)
361+
del params
362+
new_count = sys.getrefcount(sel)
363+
self.assertEqual(new_count, prev_count - 1)
364+
365+
# check for other objects as well
366+
sel1 = faiss.IDSelectorBatch([1, 2, 3])
367+
sel2 = faiss.IDSelectorBatch([4, 5, 6])
368+
sel = faiss.IDSelectorAnd(sel1, sel2)
369+
# make storage is still managed by python
370+
self.assertTrue(sel1.this.own())
371+
self.assertTrue(sel2.this.own())
372+
349373

350374
class TestSelectorCallback(unittest.TestCase):
351375

@@ -417,6 +441,7 @@ def test_12_92(self):
417441
print(j01)
418442
assert j01[0] >= j01[1]
419443

444+
420445
class TestPrecomputed(unittest.TestCase):
421446

422447
def test_knn_and_range(self):

0 commit comments

Comments
 (0)