// -*- C++ -*- // Modifications Copyright (c) 2025 Advanced Micro Devices, Inc. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #ifndef __LIBHIPTHREADS_PSEUDO_MUTEX_H__ #define __LIBHIPTHREADS_PSEUDO_MUTEX_H__ #include "hip/thread" #include "hip/hip_runtime.h" #include "hip/__support/misuse.h" #include #include /** * @file * @brief Spinning mutex with periodic pseudo_yield. * @ingroup mutex * * Similar to cuda::spin_mutex but inserts hip::this_thread::pseudo_yield() * every K failed acquire attempts (current fixed cadence) to reduce contention * impact when a critical section runs slightly longer. * * Differences vs spin_mutex: * - Adds cooperative yielding during extended spin loops. * - Slightly higher latency for extremely short uncontended locks. * - Better fairness / reduced warp residency during longer waits. * * @note pseudo_yield() has important limitations: it prevents the yielding * thread from resuming until the yieldee completes execution. This means * yield-loops (A yields to B, B tries to acquire a lock held by A) will * deadlock. See class documentation for usage constraints. * * - Non-recursive: re-entering from the same block is illegal (debug assert or * livelock). * - Single-fiber acquire: it is illegal for more than one fiber to be active * while acquiring the lock (debug assert or livelock). Extra fibers will * loop trying to acquire a lock already owned by a fiber in the same SIMD * wave/hip::wthread. Ownership is tracked using the block id so we can detect * this invalid use in debug and assert instead of livelocking. */ namespace cuda { /** * @class pseudo_mutex * @brief Busy-wait (spin) mutex with periodic pseudo_yield. * @ingroup mutex * * Characteristics: * - Exclusive, non‑recursive. * - lock(): loops atomicCAS; on repeated failure yields every 0x10000 iterations. * - try_lock(): single CAS attempt. * - unlock(): __threadfence() then atomicExch to release (establishes * release/acquire ordering with next successful lock). * * Ownership model: * - Tracks owning block via computed block id (not per-thread recursion aware). * * When to choose: * - Prefer spin_mutex for short sections with minimal contention. * - Use pseudo_mutex only when you can guarantee no yield-loops will occur. * * @warning Avoid pseudo_mutex if yield-loops are possible. A deadlock occurs * when thread A holds the lock and yields to thread B, then thread B * attempts to acquire the same lock. Since pseudo_yield() prevents * resumption until the yieldee completes, thread B cannot yield back * to thread A, causing permanent deadlock. Only use pseudo_mutex when * you can guarantee this scenario won't occur. */ class _LIBHIPTHREADS_THREAD_SAFETY_ANNOTATION(capability("pseudo_mutex")) pseudo_mutex { // HIP requires that (gridDim * blockDim) < 2^32 enum : uint64_t { INVALID_OWNER = -1ULL }; uint64_t owner = INVALID_OWNER; // stores the owner's blockId; public: /// Constructs unlocked. __device__ constexpr pseudo_mutex() = default; /// \name Deleted copy / move operations /// Instances are neither copyable nor movable. ///@{ __device__ pseudo_mutex(const pseudo_mutex &) = delete; __device__ pseudo_mutex &operator=(const pseudo_mutex &) = delete; ///@} /** * @brief Acquire (spins with periodic pseudo_yield). * * Yields every 0x10000 failed attempts. Debug assert triggers on recursive attempt * or attempting to acquire a lock already owned by another fiber in the same SIMD wave/hip::wthread. */ __device__ void lock() _LIBHIPTHREADS_THREAD_SAFETY_ANNOTATION(acquire_capability()) { const uint64_t myBlockId = blockIdx.x + gridDim.x*blockIdx.y + gridDim.x*gridDim.y*blockIdx.z; // The read operation here by atomicCAS counts as an atomic acquire operation, which the 'release fence' // operation in unlock() synchronizes-with. for (uint64_t count = 0, ownerBlockId = atomicCAS(&owner, INVALID_OWNER, myBlockId); ownerBlockId != INVALID_OWNER; ++count, ownerBlockId = atomicCAS(&owner, INVALID_OWNER, myBlockId)) { // Since execution only continues past the loop once ALL threads in a wave have exited the loop, // we need to prevent an entire wave from spinning waiting for one of it's own threads to release the lock. // // Technically this is more strict than we need to be, because we're doing this at a block level and not a // wave level, but that's fine. __HIPTHREADS_ASSERT(ownerBlockId != myBlockId, "recursive lock of a non-recursive hip::pseudo_mutex: a block already " "holding the lock tried to acquire it again (would livelock)."); if (count % 0x10000ULL == 0) { hip::this_thread::pseudo_yield(); } } } // Note that prior calls to lock() do NOT synchronize-with try_lock if it returns false! In otherwords, there is // no memory order relationship between lock and try_lock or between try_lock and itself. Only unlock() establishes // any memory order relationship. (see https://en.cppreference.com/w/cpp/thread/mutex/try_lock) /** * @brief Try to acquire without spinning. * @return true on success, false if already owned. * * Provides acquire semantics on success; no synchronizes-with edge on failure. */ __device__ bool try_lock() noexcept _LIBHIPTHREADS_THREAD_SAFETY_ANNOTATION(try_acquire_capability(true)) { const uint64_t myBlockId = blockIdx.x + gridDim.x*blockIdx.y + gridDim.x*gridDim.y*blockIdx.z; // The read operation here by atomicCAS counts as an atomic acquire operation, which the 'release fence' // operation in unlock() synchronizes-with. return atomicCAS(&owner, INVALID_OWNER, myBlockId) == INVALID_OWNER; } /** * @brief Release ownership. * * Issues device fence to publish prior writes, then clears owner with atomicExch. * Debug assert validates current block was the owner. */ __device__ void unlock() noexcept _LIBHIPTHREADS_THREAD_SAFETY_ANNOTATION(release_capability()) { // Create a 'release fence' operation which synchronizes-with the atomic acquire operation in lock() and // try_lock(). This establishes the synchronizes-with relationship required by the C++ standard: // - Unlock "synchronizes-with any subsequent lock operation that obtains ownership of the same mutex" // (see https://en.cppreference.com/w/cpp/thread/mutex/unlock) // In other words, we make sure all writes in the critical section are visible to other threads when they // acquire the mutex. __threadfence(); // This counts as the atomic store operation 'X' described in "Fence-atomic synchronization" at // https://en.cppreference.com/w/cpp/atomic/atomic_thread_fence. uint64_t oldOwner = atomicExch(&owner, INVALID_OWNER); __HIPTHREADS_ASSERT(oldOwner == blockIdx.x + gridDim.x * blockIdx.y + gridDim.x * gridDim.y * blockIdx.z, "unlock of a hip::pseudo_mutex by a block that does not own it (corrupts the " "owner state of whichever block did own it)."); } }; } // namespace cuda #endif // __LIBHIPTHREADS_PSEUDO_MUTEX_H__