Skip to content

Commit 79cd57c

Browse files
Amitay Strijevskicursoragent
andcommitted
Hoist from_array_interface wrapper class to module scope
`from_array_interface` defined its `Array` wrapper class inside the function body, so a fresh class object was created on every call. Class objects are self-referential (via `__dict__`, `__mro__` and their property descriptors), so each one forms a reference cycle that reference counting alone cannot reclaim. Because `from_array_interface` runs on every prediction, this produced cyclic garbage on every request; long-running inference services that disable the cyclic GC (a common latency optimization) saw unbounded memory growth as these cycles -- and the native buffers they pin -- accumulated. Move the wrapper to module scope as `_ArrayInterfaceProxy` so it is created once at import. The per-call instance is not part of a cycle (it references the module-level class, which does not reference it back), so it is freed immediately by reference counting with no GC needed. Behavior is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c40804f commit 79cd57c

1 file changed

Lines changed: 54 additions & 42 deletions

File tree

python-package/xgboost/_data_utils.py

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -120,52 +120,64 @@ def cuda_array_interface(data: _CudaArrayLikeArg) -> bytes:
120120
return interface_str
121121

122122

123-
def from_array_interface(interface: ArrayInf, zero_copy: bool = False) -> NumpyOrCupy:
124-
"""Convert array interface to numpy or cupy array"""
123+
class _ArrayInterfaceProxy:
124+
"""Wrapper type for communicating with numpy and cupy.
125+
126+
Defined at module scope rather than inside :py:func:`from_array_interface` on
127+
purpose. A class defined in a function body is a brand-new object on every call
128+
and participates in reference cycles (via ``__dict__``, ``__mro__`` and its
129+
property descriptors), so it cannot be reclaimed by reference counting alone.
130+
Since ``from_array_interface`` runs on every prediction, recreating the class
131+
per call produces cyclic garbage that only the cyclic GC can free, leading to
132+
unbounded memory growth in long-running services that disable the GC. Hoisting
133+
the class here means it is created once at import; the per-call instance is not
134+
part of a cycle and is freed immediately by reference counting.
135+
"""
136+
137+
_interface: Optional[ArrayInf] = None
125138

126-
class Array:
127-
"""Wrapper type for communicating with numpy and cupy."""
139+
@property
140+
def __array_interface__(self) -> Optional[ArrayInf]:
141+
return self._interface
142+
143+
@__array_interface__.setter
144+
def __array_interface__(self, interface: ArrayInf) -> None:
145+
self._interface = copy.copy(interface)
146+
# Convert some fields to tuple as required by numpy
147+
self._interface["shape"] = tuple(self._interface["shape"])
148+
self._interface["data"] = (
149+
self._interface["data"][0],
150+
self._interface["data"][1],
151+
)
152+
strides = self._interface.get("strides", None)
153+
if strides is not None:
154+
self._interface["strides"] = tuple(strides)
128155

129-
_interface: Optional[ArrayInf] = None
156+
@property
157+
def __cuda_array_interface__(self) -> Optional[ArrayInf]:
158+
return self.__array_interface__
130159

131-
@property
132-
def __array_interface__(self) -> Optional[ArrayInf]:
133-
return self._interface
160+
@__cuda_array_interface__.setter
161+
def __cuda_array_interface__(self, interface: ArrayInf) -> None:
162+
self.__array_interface__ = interface
134163

135-
@__array_interface__.setter
136-
def __array_interface__(self, interface: ArrayInf) -> None:
137-
self._interface = copy.copy(interface)
138-
# Convert some fields to tuple as required by numpy
139-
self._interface["shape"] = tuple(self._interface["shape"])
140-
self._interface["data"] = (
141-
self._interface["data"][0],
142-
self._interface["data"][1],
143-
)
144-
strides = self._interface.get("strides", None)
145-
if strides is not None:
146-
self._interface["strides"] = tuple(strides)
147-
148-
@property
149-
def __cuda_array_interface__(self) -> Optional[ArrayInf]:
150-
return self.__array_interface__
151-
152-
@__cuda_array_interface__.setter
153-
def __cuda_array_interface__(self, interface: ArrayInf) -> None:
154-
self.__array_interface__ = interface
155-
156-
@property
157-
def shape(self) -> Tuple[int, ...]:
158-
"""Shape of the input array."""
159-
aif = self.__array_interface__
160-
assert aif is not None
161-
return aif["shape"]
162-
163-
@property
164-
def size(self) -> np.signedinteger:
165-
"""Total size of the input array."""
166-
return np.prod(self.shape)
167-
168-
arr = Array()
164+
@property
165+
def shape(self) -> Tuple[int, ...]:
166+
"""Shape of the input array."""
167+
aif = self.__array_interface__
168+
assert aif is not None
169+
return aif["shape"]
170+
171+
@property
172+
def size(self) -> np.signedinteger:
173+
"""Total size of the input array."""
174+
return np.prod(self.shape)
175+
176+
177+
def from_array_interface(interface: ArrayInf, zero_copy: bool = False) -> NumpyOrCupy:
178+
"""Convert array interface to numpy or cupy array"""
179+
180+
arr = _ArrayInterfaceProxy()
169181

170182
# Cupy and numpy might run into issue when constructing an empty array from an array
171183
# interface. we explicitly check for emptiness.

0 commit comments

Comments
 (0)