-
Notifications
You must be signed in to change notification settings - Fork 111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Possible memory leak moving arrays to and from python #333
Comments
From a quick glance at your example, I think you are running into PyO3-specific issue where objects on the Python allocated from Rust have their reference counts decreased only when control returns from the Rust module which would explain your observation that:
(As you note you copy the input array for each invocation so each iteration of the nested for loops allocates one more copy of the input array from the Rust code.) To verify that this is indeed the issue, you could modify your #[pyclass(unsendable)]
struct Looper{
funcs: Vec<BoxedFunc>,
input: Array1<f64>,
output: Array1<f64>,
}
#[pymethods]
impl Looper{
#[new]
pub fn new(size: usize) -> Looper{
let funcs: Vec<BoxedFunc> = vec![];
Looper{
funcs,
input: Array1::zeros(size),
output: Array1::zeros(size),
}
}
pub fn add_func(&mut self, callable: PyObject) {
self.funcs.push(Box::new(UserDefinedFunc{callable}));
}
pub fn leek_memory(&mut self){
for func in &mut self.funcs {
func.call_func(self.input.view_mut(), self.output.view_mut());
}
self.input.assign(&self.output);
}
} The details of this scheme of managing memory are explained in the "GIL-bound memory" section of https://pyo3.rs/v0.16.4/memory.html (Btw. |
That does seem to fix it, and thanks for the tip with array views. But isn't the original situation I was in more similar to the example directly below "In general we don't want unbounded memory growth during loops! One workaround is to acquire and release the GIL with each iteration of the loop." on the page which you linked? The loop isn't inside the Python::with_gil closure? Adding the example code from below that text to the class seems to also cause a memory leak (but obviously much slower):
It seems like some python code needs to actually run between iterations of the loop? For the array example is there a standard way of doing it which avoids copies/ new allocations? I would be keen to make / add an example if there is, I couldn't find one but it seems like a common task. |
The problem here is that the GIL is already held when the
I think in your example, the solution would to not allocate bare #[pymethods]
impl Looper{
pub fn leek_memory(&mut self, size:usize, number:usize){
let input = PyArray1::<f64>::zeros(size);
let output = PyArray1::<f64>::zeros(size);
for _i in 0..number {
for func in &mut self.funcs {
func.call_func(input, output);
}
output.copy_to(input).unwrap();
}
}
}
impl Func for UserDefinedFunc{
fn call_func(&mut self, input: &PyArray1<f64>, output: &PyArray1<f64>){
Python::with_gil(|py| -> PyResult::<()> {
let py_ob = self.callable.call1(py, (input,))?;
let py_array = py_ob.as_ref(py).downcast::<PyArray1<f64>>()?;
let rust_array = unsafe { py_array.as_array() };
let output = unsafe { output.as_array_mut() };
Zip::from(output)
.and(rust_array)
.for_each(|o, &i| {*o += i});
Ok(())
});
}
} This might still leave you with the arrays that your callables allocate for their return values. But those could also be handled by providing a third |
OK that makes sense, though personally, my naive reading of the example in the memory management doc would still lead me to write leaky code by default? Is there a way to use that example code that wouldn't allocate 10 strings on the python heap as it promises not to do? Maybe this is beyond an issue now, but the problem in my case with just using PyArrays is that the looper does a lot more than this example including using external libraries that are compatible with Array1's but not PyArray1's. The 'funcs' are also not all user defined, there is a mechanism to use built in ones (using Array1s to avoid copying) for common behaviour. Maybe the best way is for looper to store two vecs of funcs: one using PyArray1s and the other using Array1s then explicitly copy the values over in the equivalent of 'looper.leak_memory'. I can't see where else the PyArrays could be owned where they wouldn't outlive the GIL. It seems a bit inelegant, though I suppose not more so than copying over for each 'func'. Thanks for your help. |
This is usually the case if this happens on separate long-lived thread that was started via e.g. Rust's The only reliable workaround that I know of for long-running computations when even
Ideally, all your functions based on EDIT: The main point is that it is possible to have Rust views into data owned by Python, but making data available to Python invalidates the invariants required by the Rust-side data structures due to pervasive sharing. (At least the possibility of sharing even if it does not actually happen. Just passing a NumPy array to a Python function increases its reference count.) |
OK that makes a lot of sense, so anything that gets passed back and forth should just be a view into python's heap presumably passed in (eg to And just to be clear, this only affects python memory allocated from rust, so the user defined python functions can be as wastefull as they want without causing a leak? |
Appears correct to me.
Yes, I think so. This affects the data is "owned via a shared reference whose lifetime is bound to the GIL", i.e. via a |
Will close this for now as we are I think all concerned are convinced that this is not actually a memory leak. But please feel free to continue the discussion here or at https://github.com/PyO3/rust-numpy/discussions |
Thanks @adamreichold for the very helpful information shared here. Just two things I'd like to add:
|
Hi I brought this up on the gitter and people said to post here. It seems like there is a memory leak when moving arrays between rust and python.
I have made an minimal example repo here:
https://github.com/mikeWShef/leak
I am explicitly copying the arrays using into_pyarray and as_array, but as far as I know this should not be an issue.
The jupyter notebook example will quickly fill >30GB of memory when really there are only a maximum of 6 arrays of 2048**2 float64 so ~200MB.
When the rust program ends the memory is cleaned up again, but adding gc.collect() into the python function dosn't fix the leek.
Windows 11, python 3.8, numpy 1.20.3 tested
The text was updated successfully, but these errors were encountered: