Metal rendering application is not releasing resources

I am developing a metal based ray tracing rendering application (running heavy GPU kernels). I am sometimes "forcefully quitting" my application and I can see the application is not in the activity monitor. But I can see the windowserver is using %97 the GPU. The mac gets hotter and hotter. I kill the windowserver, re-login it is still the case. The only way to fix is to restart the mac. I have checked if there are any zombie processes, there are none. I am 3-4 month into Mac development (I used many rendering APIs e.g. before under Windows and Linux, they release the resources automatically unless the driver is very broken), but I believe when you force quit or exit gracefully, regarding application should release resources. I may be missing some knowledge. Does anybody have an idea?

I had added every corner a graceful exit code but once the kernel has some infinite loop the clean up cannot happen. In Windows there are some driver reload mechanisms to recover when GPU is stuck, is there a similar system ?

Answered by DTS Engineer in 899812022

Thanks for the detailed description, and you have already identified the key piece: the unbounded (infinite-loop) GPU work.

Here is what the documentation supports, with references.

A committed Metal command buffer runs on the GPU asynchronously (see Setting up a command structure and MTLCommandBuffer). The Metal API provides commit and status reporting. It has no call to cancel a compute or render command buffer once you commit it, and no call to reset the GPU from your app. The only cancellation in the API is tryCancel(), and that is on MTLIOCommandBuffer, the file input/output queue, not a compute or render queue. So quitting or force-quitting your app does not give you a way to recall work the GPU has already begun, and with an infinite kernel that work has no natural end.

The system does act on command buffers that run too long, which is the closest analog to the Windows behavior you mention. This is documented on MTLCommandBufferError:

  • timeout: "An error code that indicates the system interrupted and terminated the command buffer before it finished running."
  • accessRevoked: "An error code that indicates the system has revoked the Metal device's access because it's responsible for too many timeouts or hangs."

So the system can terminate an overlong command buffer, and can cut off a client that causes too many. What it does not offer is a call you invoke yourself to do either.

You can check what your own command buffers report. Read status and error on the command buffer, for example in a completed handler. If the system has terminated your work, error will be MTLCommandBufferError.timeout or .accessRevoked. If you do not see those, that is a useful clue in itself.

The reliable approach is to keep each GPU submission bounded, so nothing runs long enough to be terminated in the first place. Rather than one open-ended dispatch, submit the ray-tracing work as a series of smaller command buffers and keep the loop on the CPU, deciding between submissions whether to continue (see Setting up a command structure). Each submission then stays short and always finishes, stopping becomes a matter of not enqueuing the next one, and other work on the shared GPU can proceed between submissions.

On the graceful-exit code you added: the instinct is right, but because the command buffer executes asynchronously after you commit it, exit code that runs afterward is already too late to affect work in flight. The place to stop is between submissions, before you enqueue the next chunk.

If you find the system does not recover after your process exits, that is worth a report through Feedback Assistant.

Hope that helps.

Accepted Answer

Thanks for the detailed description, and you have already identified the key piece: the unbounded (infinite-loop) GPU work.

Here is what the documentation supports, with references.

A committed Metal command buffer runs on the GPU asynchronously (see Setting up a command structure and MTLCommandBuffer). The Metal API provides commit and status reporting. It has no call to cancel a compute or render command buffer once you commit it, and no call to reset the GPU from your app. The only cancellation in the API is tryCancel(), and that is on MTLIOCommandBuffer, the file input/output queue, not a compute or render queue. So quitting or force-quitting your app does not give you a way to recall work the GPU has already begun, and with an infinite kernel that work has no natural end.

The system does act on command buffers that run too long, which is the closest analog to the Windows behavior you mention. This is documented on MTLCommandBufferError:

  • timeout: "An error code that indicates the system interrupted and terminated the command buffer before it finished running."
  • accessRevoked: "An error code that indicates the system has revoked the Metal device's access because it's responsible for too many timeouts or hangs."

So the system can terminate an overlong command buffer, and can cut off a client that causes too many. What it does not offer is a call you invoke yourself to do either.

You can check what your own command buffers report. Read status and error on the command buffer, for example in a completed handler. If the system has terminated your work, error will be MTLCommandBufferError.timeout or .accessRevoked. If you do not see those, that is a useful clue in itself.

The reliable approach is to keep each GPU submission bounded, so nothing runs long enough to be terminated in the first place. Rather than one open-ended dispatch, submit the ray-tracing work as a series of smaller command buffers and keep the loop on the CPU, deciding between submissions whether to continue (see Setting up a command structure). Each submission then stays short and always finishes, stopping becomes a matter of not enqueuing the next one, and other work on the shared GPU can proceed between submissions.

On the graceful-exit code you added: the instinct is right, but because the command buffer executes asynchronously after you commit it, exit code that runs afterward is already too late to affect work in flight. The place to stop is between submissions, before you enqueue the next chunk.

If you find the system does not recover after your process exits, that is worth a report through Feedback Assistant.

Hope that helps.

First of thanks for the detailed answer. I started using the Macos 27 dev 4 version and I have noticed the unbounded programs issue is handled by the OS. I have killed the process which is stuck and I saw the GPU usage went back to 0. Is it the case ?

Metal rendering application is not releasing resources
 
 
Q