ENH: spatial.KDTree: make thread-safe via immutability#24799
ENH: spatial.KDTree: make thread-safe via immutability#24799rgommers merged 10 commits intoscipy:mainfrom
Conversation
| assert_array_equal(t.maxes, np.amax(points, axis=0)) | ||
| assert_array_equal(t.mins, np.amin(points, axis=0)) | ||
| assert t.data is points | ||
| assert t.data.base is points |
There was a problem hiding this comment.
This is due to chaging data to be a read-only view. I'm not sure if it counts as a breaking change.
There was a problem hiding this comment.
I played with this feature branch a bit locally, and T.data does still provide access to the points of course, so seems like it would be a mild break at worst.
There might even be a bug fix argument I suppose since it prevents a footgun. Maybe we could ping Sturla before merging if we're worried about it.
There was a problem hiding this comment.
That shouldn't count as a breaking change, more like a robustness improvement. The current docstring of data input argument already says: "This array is not copied unless this is necessary to produce a contiguous array of doubles, and so modifying this data will result in bogus results."
| # to avoid deadlocks if spawning failed for some reason | ||
| barrier.abort() | ||
|
|
||
| return [f.result() for f in futures] |
There was a problem hiding this comment.
The original implementation can deadlock if spawning one of the threads fails. I fixed that and also used a ThreadPoolExecutor, which allows me to return results from the worker functions.
|
None of the CI failed due to using a too-new Cython feature. Maybe it is OK to bump the minimum Cython version? |
It's okay to bump Cython to >=3.2. Ideally a Cython bug fix release with the compiler warning fixed would show up, but we can suppress the Line 569 in a6e37c0 and then add that to the extension module's |
|
This looks pretty good, thanks for working on this @ngoldbaum! I'll have a closer look once it's out of draft. |
018a536 to
e0ab9df
Compare
Works around a Cython issue that will be addressed in the next bugfix release.
e0ab9df to
1e3fe8d
Compare
| assert_array_equal(t.maxes, np.amax(points, axis=0)) | ||
| assert_array_equal(t.mins, np.amin(points, axis=0)) | ||
| assert t.data is points | ||
| assert t.data.base is points |
There was a problem hiding this comment.
I played with this feature branch a bit locally, and T.data does still provide access to the points of course, so seems like it would be a mild break at worst.
There might even be a bug fix argument I suppose since it prevents a footgun. Maybe we could ping Sturla before merging if we're worried about it.
| # read-only view so ban people modifying tree.data after the tree is | ||
| # constructed | ||
| data = data.view(np.ndarray) | ||
| data.flags.writeable = False |
There was a problem hiding this comment.
I did wonder when I was reading this if the need for this is milder with copy_data=True? I guess you still have problems, even if a little less serious.
There was a problem hiding this comment.
It's still useful to set this read-only flag, as it insulates against mutating this .data attribute. It's milder indeed in that copy_data already insulates against the base array for the input data being mutated. But this seems like the right thing to do either way.
|
I think this is ready now. I bumped the minimum version to 3.2.0 because I wasn't sure downstream builders would necessarily have 3.2.4. Let me know if you'd prefer it to be 3.2.4. |
|
The last commit fixes a possible race that David Woods pointed out to me: cython/cython#7581 (comment). We could go back to having a boolean flag but I'd need to use c++ standard library atomics to do that. I don't think the overhead of creating the wrapper object on every call matters much. |
Eh, the atomics aren't so bad, see the last example in cython/cython#7582. Should I just go ahead and do that, along with switching to a closure instead of a wrapper object? |
|
I think it's possible to avoid the closure and the atomics by using a global |
|
@ngoldbaum given the Cython bug fix and discussion around avoiding wrapper, can you confirm whether the minimum Cython version this PR needs in its current form is 3.2.0 or 3.2.5? |
The version here should work with 3.2.0 I believe |
I just double-checked: this branch builds in an environment with Cython 3.2.0 installed. |
…7582) Based on my experience working on scipy/scipy#24799.
rgommers
left a comment
There was a problem hiding this comment.
I went over this again, and all looks good - time to get it in. Thanks @ngoldbaum & reviewers!
| # read-only view so ban people modifying tree.data after the tree is | ||
| # constructed | ||
| data = data.view(np.ndarray) | ||
| data.flags.writeable = False |
There was a problem hiding this comment.
It's still useful to set this read-only flag, as it insulates against mutating this .data attribute. It's milder indeed in that copy_data already insulates against the base array for the input data being mutated. But this seems like the right thing to do either way.
|
ASan CI has been a bit shaky since this PR was merged, not sure if it's related at all but either way maybe somebody involved here would be well-placed to investigate #24980 |
Towards fixing #20669. Supersedes #20655.
Currently, scipy.spatial.KDTree is mostly thread-safe. There are two major issues right now that this PR helps to fix:
KDTree.treeis lazily-initialized, which can lead to data races.I've converted
KDTree.treeto use apy_safesingle-initialization API, as described here in the Cython docs. These are new in Cython 3.2, so I bumped the minimum Cython version. There's also a compiler warning I can't fix that I reported to Cython.I also made the ndarray attributes of the KDTree class be read-only. While there are still ways to modify a read-only NumPy array, this at least makes it harder for users to shoot themselves in the foot. I clarified the docstring for the
dataargument to expand the warnings about modifying the data if no copy happens. I noticed that theboxsizeattribute is undocumented, so I added entries. I also updated the thread safety docs.Finally, I added a multithreaded
query_ball_pointtest and a multithreadedKDTree.treegeneration test. I can add multithreaded tests for more of the query functions if you'd like me to do that. I also added a test to validate that KDTrees cannot be modified from Python.I also updated the multithreaded test helper to fix a possible deadlock and allow tests to get return values from worker threads without relying on global state.
#20655 has a lot of discussion about modifying the KDTree before
__init__or__cinit__finishes. I'm pretty sure that's all a red herring. It's not possible to modify an object before the initializer finishes, and initializers run in an effectively single-threaded context. I think confusion around that point led to the choice of adding a lock. I don't think that's actually needed.I ran the KDTree tests, including the new multithreaded tests, under thread sanitizer on my Mac and see no data races.