GeezerVison is showing github.com — Garbage collection when a resource is no longer in use · Issue #1264 · pygfx/pygfx
Garbage collection when a resource is no longer in use #1264DescriptionWhen a resource is no longer in use, the references to it are cleared (i.e. the resource is longer referenced to in engine.Binding) but it still lingers in GPU VRAM until GPUBuffer.destroy() is called. But I'm guessing this is not recommended?
Example, run this in ipython for example, with %gui qt. It just adds a Line with 50 million points which takes up ~600MB of VRAM, and then replaces the buffer with data that has only 200 points so we should see GPU VRAM drop significantly.
import pygfx
import numpy as np
from rendercanvas.auto import RenderCanvas, loop
from rendercanvas.utils.asyncs import sleep
import wgpu
from pygfx.renderers.wgpu import select_adapter
import gc
# just choosing my nvidia GPU so I can use nvidia-smi to see RAM usage
select_adapter(wgpu.gpu.enumerate_adapters_sync()[1])
class Scope:
def __init__(self):
data = np.random.rand(50_000_000, 3).astype(np.float32)
self.canvas = RenderCanvas()
self.renderer = pygfx.WgpuRenderer(target=self.canvas)
self.original_buffer = pygfx.Buffer(data)
self.line = pygfx.Line(
pygfx.Geometry(positions=self.original_buffer),
pygfx.LineMaterial()
)
self.scene = pygfx.Scene()
self.scene.add(self.line)
self.camera = pygfx.PerspectiveCamera(fov=0)
self.camera.show_object(self.scene)
self.canvas.request_draw(self.render)
def render(self):
self.renderer.render(self.scene, self.camera)
self.canvas.request_draw()
def change(self):
data2 = np.random.rand(200, 3).astype(np.float32)
self.line.geometry.positions = pygfx.Buffer(data2)
def check_referrers(self):
print(gc.get_referrers(self.original_buffer))
def delete_old_buffer(self):
self.original_buffer._wgpu_object.destroy()
# this alone is not sufficient to release GPU VRAM
del self.original_buffer
s = Scope()
loop.run()
Now go through the following:
We get 3 referrers which makes sense, the Scope instance, Geometry and Binding.
[<__main__.Scope object at 0x7f5268fac830>, Geometry(
positions=<pygfx.resources._buffer.Buffer object at 0x7f5260622350>,
) # at 0x7f526817e260, <pygfx.renderers.wgpu.engine.binding.Binding object at 0x7f526895e470>]
Call change() to replace the buffer. Then interact with the canvas to make sure new frames are drawn etc:
If we check the referrers we can see that the only lingering reference is to the Scope instance which makes sense, it's no longer referenced in the Binding and Geometry:
[<__main__.Scope object at 0x7f5268fac830>]
Finally if we call s.delete_old_buffer(), then GPU VRAM is cleared since it calls original_buffer._wgpu_object.destroy() and removes the original_buffer reference from the Scope instance as well. However if we only remove the reference to the buffer, then it lingers in GPU VRAM, even though it's been removed from Binding.
Is this expected behavior? If not what's the best way to destroy the GPUBuffer when a Binding resource changes? Activityalmarklein commented on Jan 23, 2026
However if we only remove the reference to the buffer, then it lingers in GPU VRAM, even though it's been removed from Binding.
The expectation is that removing the reference to the buffer will also release the VRAM, although it could linger a little longer, because the gc will need to kick in.
What should happen:
- Once the
original_buffer is removed, it has no more references, so it is cleaned up by gc.
- Which will cause it's internal
_wgpu_object attribute, the wgpu.GPUBuffer, to be de-referenced as well.
- Which causes it's
__del__ to be called, which calls 'destroy()`.
I did some quick tests with your code, adding
ref = weakref.ref(s.original_buffer._wgpu_object)
And from what I see, that object is indeed dead after calling delete_old_buffer, also if I comment the line that explicitly calls destroy().
Are you saying you are seeing something different when you inspect the VRAM? If you do, try calling gc.collect(). kushalkolar commented on Feb 2, 2026 ContributorAuthor More actions
Are you saying you are seeing something different when you inspect the VRAM? If you do, try calling gc.collect().
Yes!
Simplified the example to show what I mean:
import pygfx
import numpy as np
from rendercanvas.auto import RenderCanvas, loop
from rendercanvas.utils.asyncs import sleep
import wgpu
from pygfx.renderers.wgpu import select_adapter
import gc
import weakref
# just choosing my nvidia GPU so I can use nvidia-smi to see RAM usage
select_adapter(wgpu.gpu.enumerate_adapters_sync()[1])
class Scope:
def __init__(self):
data = np.random.rand(50_000_000, 3).astype(np.float32)
self.canvas = RenderCanvas()
self.renderer = pygfx.WgpuRenderer(target=self.canvas)
self.line = pygfx.Line(
pygfx.Geometry(positions=pygfx.Buffer(data)),
pygfx.LineMaterial()
)
self.original_buffer = weakref.proxy(self.line.geometry.positions)
self.scene = pygfx.Scene()
self.scene.add(self.line)
self.camera = pygfx.PerspectiveCamera(fov=0)
self.camera.show_object(self.scene)
self.canvas.request_draw(self.render)
def render(self):
self.renderer.render(self.scene, self.camera)
self.canvas.request_draw()
def change(self):
data2 = np.random.rand(200, 3).astype(np.float32)
# VRAM is not freed unless this line is called
# self.line.geometry.positions._wgpu_object.destroy()
self.line.geometry.positions = pygfx.Buffer(data2)
s = Scope()
loop.run()
VRAM not freed if we don't call destroy, even though the reference to the original buffer no longer exists:
gc_buffer-2026-02-02_18.06.51.mp4
VRAM freed only if we call destroy:
gc_buffer_destroy-2026-02-02_18.07.59.mp4
kushalkolar commented on Feb 4, 2026 ContributorAuthor More actions More interesting stuff, I made this example with fastplotlib in the branch of this PR fastplotlib/fastplotlib#974 .
Not using wgpu_obj.destroy(). It just dereferences, no gc.collect() either and it seems like VRAM is freed as expected. Ben running this for a long while (10+ mins) and there is no memory leak. I don't understand why it's working here though, but not in the above example. Does the resource get cached somehow and it's only cleared when we're constantly allocating new buffers or something?
By the way I am impressed at the performance even when we're constantly replacing the buffer as shown here. Every time n_points changes, it's allocating a new buffer 😂 . pygfx and wgpu are the bestest ❤️
no_destroy-2026-02-03_21.29.02.mp4
import numpy as np
import fastplotlib as fpl
xs = np.linspace(0, 10 * np.pi, 100)
ys = np.sin(xs)
data = np.column_stack([xs, ys])
fig = fpl.Figure(shape=(2, 2), size=(700, 700))
line = fig[0, 0].add_line(data)
text = fig[0, 0].add_text(f"n_points: {100}", offset=(0, 3, 0), anchor="middle-left")
scatter = fig[0, 1].add_scatter(
np.random.rand(100, 3),
colors=np.random.rand(100, 4),
sizes=(np.random.rand(100) + 1) * 3,
edge_colors=np.random.rand(100, 4),
point_rotations=np.random.rand(100) * 180,
uniform_size=False,
uniform_edge_color=False,
point_rotation_mode="vertex",
)
fig.show()
i = 0
def update():
global i
# update line
freq = (np.sin(i) + 1) * 5
n_points = int((freq * 50_000) + 10)
xs = np.linspace(0, 10 * np.pi, n_points)
ys = np.sin(xs * freq)
new_data = np.column_stack([xs, ys])
line.data = new_data
# update scatter
scatter.data = np.random.rand(n_points, 3)
scatter.colors = np.random.rand(n_points, 4)
i += 0.01
text.text = f"n_points: {n_points}"
fig.add_animations(update)
fpl.loop.run()almarklein commented on Feb 9, 2026 It's also worth mentioning that calling destroy kills the buffer at once (wgpuBufferDestroy). While when the object is gc'd naturally, it is released (wgpuBufferRelease). Destroying is not recommended in general, because if a pipeline object still uses the buffer, that pipeline is now broken, and wgpu will produce an error if you try to use it.
This may also explain why the first example is 'slow' to release VRAM. Apart from us releasing the buffer, any pipeline object will also have to be released, before the buffer can really be cleaned up by wgpu-core. So at the very least we need a draw, preferably a couple of draws, to tick passed Pygfx' caching. kushalkolar commented on Mar 4, 2026 ContributorAuthor More actions I'm going to close this, the first example still doesn't seem to free gpu VRAM but the second example is much more realistic and reflects our use case as well. In real usage gc is working. Can revisit this if gc is an issue in a real use case.
Thanks for the details! Helped to understand the gc system better.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|