From 0b41618b97545a8bdfa2a126f4b80c9bd712632c Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Tue, 22 Sep 2026 20:06:40 +0200 Subject: [PATCH 1/2] Fix Metal buffer leak: register PyObjC ownership for MTLDevice new* methods PyObjC's Metal bindings have no ownership metadata for the MTLDevice protocol's new* selectors, so the +1 return was treated as autoreleased and never released. Every per-batch MTLBuffer leaked for the life of the process (300k seeds: 16.5 GB peak footprint -> 6.1 GB, 9.8 GB max RSS -> 5.3 GB). Co-Authored-By: Claude Fable 5.1 --- cuslines/metal/mutils.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cuslines/metal/mutils.py b/cuslines/metal/mutils.py index d190e59..91fbfb9 100644 --- a/cuslines/metal/mutils.py +++ b/cuslines/metal/mutils.py @@ -73,6 +73,34 @@ class ModelType(IntEnum): PAGE_SIZE = 16384 +# PyObjC's Metal bindings carry no ownership metadata for the MTLDevice +# protocol's new* methods, so PyObjC treats their +1 return as autoreleased +# and never releases it: every per-batch MTLBuffer leaked (~19 GB / 588k seeds). +# registerMetaDataForSelector replaces the inferred metadata, so pointer and +# out-arguments must be restated here. +def _register_metal_ownership(): + import objc + + ptr = {"type": b"n^v", "c_array_length_in_arg": 3} + err = {"type": b"o^@"} + for sel, args in ( + (b"newBufferWithLength:options:", {}), + (b"newBufferWithBytes:length:options:", {2: ptr}), + (b"newCommandQueue", {}), + (b"newLibraryWithSource:options:error:", {4: err}), + (b"newFunctionWithName:", {}), + (b"newComputePipelineStateWithFunction:error:", {3: err}), + ): + objc.registerMetaDataForSelector( + b"NSObject", + sel, + {"retval": {"already_retained": True}, "arguments": args}, + ) + + +_register_metal_ownership() + + def div_up(a, b): return (a + b - 1) // b From e177e5aa0aea4f9cf3e4a375824a7bf0d3a59fad Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Tue, 22 Sep 2026 20:19:29 +0200 Subject: [PATCH 2/2] CI: only push Docker image on push events Fork PRs have no write token for the org GHCR package, so the pull_request build always failed at the push step (see #43 as well). Co-Authored-By: Claude Fable 5.1 --- .github/workflows/dockerbuild.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dockerbuild.yml b/.github/workflows/dockerbuild.yml index 30b75c5..d501a96 100644 --- a/.github/workflows/dockerbuild.yml +++ b/.github/workflows/dockerbuild.yml @@ -31,7 +31,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - push: true + push: ${{ github.event_name != 'pull_request' }} tags: | ghcr.io/dipy/gpustreamlines:${{ github.sha }} ghcr.io/dipy/gpustreamlines:latest