You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The lock and flag an asynchronous task synchronises on are plain volatile ints,
written by both threads with no ordering imposed. `volatile` guarantees the
loads are re-issued; it does not order stores, so on a weakly ordered target
they can be observed out of order and the handshake loses an update:
compute: lock0[0] = 0; (release_lock0) -- observed late
sdata0->flag = 2; (activate0) -- observed first
task: sees the request, delivers lock0[0] = 2, sets flag = 1
compute: the late lock0[0] = 0 lands, wiping the delivery
the next release_lock0 waits for a 2 nobody will write again
Both threads then spin forever: the compute waiting for data whose request it
has already spent, the task waiting to be asked. `lock == 0 && flag == 1` is
reachable only this way -- the task sets the lock before the flag, so a
completed cycle must leave the lock at 2 -- and that is the state a stalled run
sits in.
Fenced on both sides: a release before the flag that publishes a request or a
completion, an acquire before reading what either stands for. `__atomic_thread_fence`
is a builtin, valid in C and C++ alike, so the device targets that render
through CXXPrinter are unaffected; declaring the two objects `_Atomic` would
have been the standard-clean alternative but that is not valid C++ under g++.
Found through a streaming-checkpoint FWI gradient on arm64, where it stalled one
worker in six within minutes and, in the same runs, had CvxCompress trip its own
assertion on a buffer the task never filled. x86's store ordering hides it.
The tests index the two callables positionally, so they move with the fences.
0 commit comments