First, if anyone is interested in using Python from within Swift without the TensorFlow part, you can easily integrate the Python interop part of the TensorFlow code by getting it from here https://github.com/pvieito/PythonKit I’ve been using it for many months now and they seem to keep it pretty well in sync with the TensorFlow repo.
Back when Jeremy, Sylvain and many others started working on FastAI 1.0, I foolishly decided I’d try to learn the inner workings of FastAI by attempting to code it in Swift as they coded it in Python and so I started using the aforementioned PythonKit.
It’s been a great learning experience but I’ve had issues with memory not being released, so finally this weekend I decided to solely work on finding and fixing these memory leaks if at all possible and after much searching I believe I’ve found a few causes and so wanted to share them with you as it seems the FastAI community is actively embracing Swift with much enthusiasm and you can’t get much deep learning done if you run out of memory. I haven’t fixed all my memory leaks and if I find any more in this library I’ll post here. Also, if anyone can either corroborate my findings or tell me why I’m wrong that would be great.
Below are the changes I believe you need to make in Python.swift
1. In the 2 functions dynamicallyCall, add defer { Py_DecRef(result) } right before the return statement like below. I believe this is necessary because PyObject_Call and PyObject_CallObject according to the Python documentation return a reference that has already been incremented but the PythonObject constructor (the owning constructor) being used in this code is also incrementing the reference.
guard let result = PyObject_CallObject(selfObject, argTuple) else {
// If a Python exception was thrown, throw a corresponding Swift error.
try throwPythonErrorIfPresent()
throw PythonError.invalidCall(base)
}
defer { Py_DecRef(result) } // <-- Add this line
return PythonObject(owning: result)
}
2. Rewrite the function performBinaryOp to the following to use defer to decrement the lhs, rhs and the result. I believe these changes are necessary because original code was passing ownedPyObjects to the operator and ownedPyObject increments the pointer reference but there was no code to decrement the references after the operation finishes. Also, I believe (but couldn’t find in documentation), that the operator’s return value already has an incremented reference and the PythonObject(owning) constructor increments it again thus it has a reference count of 2 instead of 1 and therefore needs to be decremented before being returned to the caller.
private func performBinaryOp(_ op: PythonBinaryOp, lhs: PythonObject, rhs: PythonObject) -> PythonObject {
// let result = op(lhs.ownedPyObject, rhs.ownedPyObject) <-- Replace this line with below
let lhs = lhs.ownedPyObject
let rhs = rhs.ownedPyObject
defer {
Py_DecRef(lhs)
Py_DecRef(rhs)
}
let result = op(lhs, rhs)
// If binary operation fails (e.g. due to `TypeError`), throw an exception.
try! throwPythonErrorIfPresent()
defer { Py_DecRef(result) } // <-- Add this line
return PythonObject(owning: result!)
}
3. One other tip is to make sure to call the close() function on any PIL images when you are done with them. I’m not sure yet if this is a side effect of references not being released correctly and once these are all fixed than that won’t be necessary anymore or not. Still investigating.
Happy Swifting!