Thread (16 messages) 16 messages, 4 authors, 2d ago

Re: [RFC PATCH v2 0/3] Solve dma_fence's problems by locking

flat view

From: Christian König <christian.koenig@amd.com>
Date: 2026-10-21 11:54:10
Also in: dri-devel, intel-gfx, linux-media, lkml, rust-for-linux

On 10/20/26 17:36, Philipp Stanner wrote:
...
quoted
quoted
quoted
First of all that prevents concurrent execution of the callbacks
which for a lot of use cases is a must have.
Currently only set_deadline() can execute in parallel.
Especially the is_signaling() callback is executed in parallel by tons of use cases.
ops->signaled() you mean.

Anyways, only one will succeed at having the fence marked as signaled
anyways, and then all other callers of dma_fence_is_signaled() will be
guarded by the fastpath bit-check (which btw is also broken; but we
want to address that in a different patch. For now my series proposes
keeping the fastpath).
What should be broken here? As far as I know the signaling path works perfectly fine.

What we have problems with is the lifetime of things.
quoted
quoted
IIUC in our past discussions, your wish has been that the driver can
take its own locks with free locking-order in the callbacks.

Thus, the callback is invoked under lock in any case, just a few
instructions earlier or later. Thus, no callback ever truly executes
concurrently.
And that is fine. Taking the look when you actually need it to
protect data structures is perfectly correct.

But what you propose here is to abuse the look as a barrier, e.g. to
find out if something has finished or not and that is a completely
different story.
Nope, I want to protect shared data, which is the signaled-state. Which
is currently a race-condition worked around with RCU (and sometimes
with manually doing the right thing (locking!)) [1], stemming from the
fact that the fence's spinlock is not used consistently.
The fence spinlock is consistently used for protecting the fence signaling, in other words it always happens with the spinlock held, same for adding callbacks.

The only thing which happens without the spinlock held is the fast path checking of the signaled and enable_signaling bits and as far as I can see that is ok as well.

The problem here is not about the state of the fence, but if the callbacks are still executed or not. And that is not a good task for a lock, instead what is usually used for that is a barrier and that is exactly what RCU provides. 
quoted
quoted
quoted
Then as I pointed out before the second problem is that this again
makes the existing lock inversion between the signaling by polling
and signaling by interrupts even worse than it already is.
We asked for examples in our last discussions, and the only one ever
presented was the list-access requiring the fctx-lock in
nouveau_fence_no_signaling(). Nouveau is often not a good example.

Can you provide examples in the kernel where deadlock would occur?
Just exercise the simple example of a driver supporting both pull and interrupt:

Interrupt: Handler takes lock protecting a linked list, finds fence
and signals it.

Polling: is_signaled is called with fence lock held, can't take list
lock and so needs to work around that somehow.
signaled() neither needs a list lock nor any other lock.
I don't think we should decide that inside the framework, this is up to the implementation of the dma_fence backend.

By taking the lock for the callbacks we just move the complexity of handling from the framework into the dma_fence_ops implementations, and that is something I would *really* like to avoid.
It needs to read a sequence number from somewhere, so a u64. And then
it returns true or false to the fence_backend.

The u64 can easily be an atomic. Also ioremaped reads should work
locklessly.

No need for any lock.
It is perfectly possible that you need to take whatever lock you want to cleanup things. E.g. spinlock for list removal, common lock with the interrupt handler etc....

That's also why I said that the whole approach with the signaling and enable_signaling callback returning a boolean is broken.

Instead what we should do is to forward the requests to the dma_fence implementation. And that dma_fence implementation calls dma_fence_signaled() as necessary.
quoted
This pattern repeats multiple times all over the different dma_fence
implementation. Nouveu is just the tip of the iceberg, we have cases
of this in amdgpu as well as i915 as well as XE.
I guess my question then becomes what complicated locked tasks someone
has to do within its callback that locks are required, locks that
cannot be partitioned in a smart way to avoid that.
Simple cleanup tasks, like just list_del() you need to take a spinlock for so.
We have 3 callbacks that are relevant and not deprecated (the name-
functions are trivially solved by copying the data):

- is_signaled() ⇒ solved, see above
- enable_signaling()
- set_deadline()

The latter two might want to talk to their hardware, either enabling an
interrupt or telling it to clock the frequency up (see MSM). They might
need a driver-lock for that, fair enough.

Where does such a driver-lock need to be held while operating on a
fence, opening the deadlock-path?
In the interrupt handler.
Note that, as I pointed out many times, all callbacks are already
called under lock-protection. Especially enable_signaling(), and
is_signaled() must be lock-resistant, proven by
dma_fence_is_signaled_locked() existing.
Yes, but we clearly want to get away from that.

See the calling order in the interrupt signaling path is driver -> framework.

While in the is_signaled/enable_signaling/set_deadline path it is framework -> driver.

The dma_fence framework solved this issue by using a shared external driver provided lock, and that lock is then taken before calling into the backend operations. But we want to get away from that and use an inline lock in each dma_fence.

So the only two possibilities I can see is to either call the backend without holding locks or force the backend operations to use a lockless approach for their internal handling.
Regardless, what I propose would only make a potential difference for
*new* users, with inline-lock, who can then cleanly design to avoid
locking problems.
Yeah, but I'm desperately trying to avoid the complexity of using a lockless approach in the driver implementation.

Just take a look at amdgpu_fence.c and how complex the RCU handling there becomes because of this issue here.

Or the dma_fence_chain and dma_fence_array containers, same problem there as well.
quoted
quoted
Note that I pointed out that all callbacks are already invoked with the
lock taken, which proofs that it's doable.
Again, no it isn't. The shared lock was invented exactly because
drivers ran into this problem.

The common pattern is the same, implementations just dance around the
fact that the callbacks are called with the fence lock held by
delegating cleanup work and/or using lockless containers etc...
I suspect more that drivers attempt to do unnecessarily many things in
their callbacks which are not supposed to be done there.
quoted
A common component like dma_fence should never enforce users to do
such workarounds just to solve it's own lifetime issues.
Our common component forces all users to take our locks manually and
plaster everything with RCU. If they forget an RCU delay, outside our
API, things explode :(
And I still think that this is the simpler and especially more defensive approach.

The dma_fence framework should not leak it's internal lock anywhere externally and that includes calls into the backend operations.

Regards,
Christian.

P.


[1] https://elixir.bootlin.com/linux/v7.3/source/drivers/gpu/drm/amd/amdgpu/amdgpu_vm.c#L2787
quoted
  
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help