Skip to content

[SYCL] move more common utils from pi_level_zero #7821

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 0 additions & 95 deletions sycl/plugins/level_zero/pi_level_zero.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,101 +65,6 @@ template <> uint32_t inline pi_cast(uint64_t Value) {
return CastedValue;
}

// The wrapper for immutable Level-Zero data.
// The data is initialized only once at first access (via ->) with the
// initialization function provided in Init. All subsequent access to
// the data just returns the already stored data.
//
template <class T> struct ZeCache : private T {
// The initialization function takes a reference to the data
// it is going to initialize, since it is private here in
// order to disallow access other than through "->".
//
using InitFunctionType = std::function<void(T &)>;
InitFunctionType Compute{nullptr};
bool Computed{false};
pi_mutex ZeCacheMutex;

ZeCache() : T{} {}

// Access to the fields of the original T data structure.
T *operator->() {
std::unique_lock<pi_mutex> Lock(ZeCacheMutex);
if (!Computed) {
Compute(*this);
Computed = true;
}
return this;
}
};

// This wrapper around std::atomic is created to limit operations with reference
// counter and to make allowed operations more transparent in terms of
// thread-safety in the plugin. increment() and load() operations do not need a
// mutex guard around them since the underlying data is already atomic.
// decrementAndTest() method is used to guard a code which needs to be
// executed when object's ref count becomes zero after release. This method also
// doesn't need a mutex guard because decrement operation is atomic and only one
// thread can reach ref count equal to zero, i.e. only a single thread can pass
// through this check.
struct ReferenceCounter {
ReferenceCounter() : RefCount{1} {}

// Reset the counter to the initial value.
void reset() { RefCount = 1; }

// Used when retaining an object.
void increment() { RefCount++; }

// Supposed to be used in pi*GetInfo* methods where ref count value is
// requested.
pi_uint32 load() { return RefCount.load(); }

// This method allows to guard a code which needs to be executed when object's
// ref count becomes zero after release. It is important to notice that only a
// single thread can pass through this check. This is true because of several
// reasons:
// 1. Decrement operation is executed atomically.
// 2. It is not allowed to retain an object after its refcount reaches zero.
// 3. It is not allowed to release an object more times than the value of
// the ref count.
// 2. and 3. basically means that we can't use an object at all as soon as its
// refcount reaches zero. Using this check guarantees that code for deleting
// an object and releasing its resources is executed once by a single thread
// and we don't need to use any mutexes to guard access to this object in the
// scope after this check. Of course if we access another objects in this code
// (not the one which is being deleted) then access to these objects must be
// guarded, for example with a mutex.
bool decrementAndTest() { return --RefCount == 0; }

private:
std::atomic<pi_uint32> RefCount;
};

// Base class to store common data
struct _pi_object {
_pi_object() : RefCount{} {}

// Level Zero doesn't do the reference counting, so we have to do.
// Must be atomic to prevent data race when incrementing/decrementing.
ReferenceCounter RefCount;

// This mutex protects accesses to all the non-const member variables.
// Exclusive access is required to modify any of these members.
//
// To get shared access to the object in a scope use std::shared_lock:
// std::shared_lock Lock(Obj->Mutex);
// To get exclusive access to the object in a scope use std::scoped_lock:
// std::scoped_lock Lock(Obj->Mutex);
//
// If several pi objects are accessed in a scope then each object's mutex must
// be locked. For example, to get write access to Obj1 and Obj2 and read
// access to Obj3 in a scope use the following approach:
// std::shared_lock Obj3Lock(Obj3->Mutex, std::defer_lock);
// std::scoped_lock LockAll(Obj1->Mutex, Obj2->Mutex, Obj3Lock);
pi_shared_mutex Mutex;
};

// Record for a memory allocation. This structure is used to keep information
// for each memory allocation.
struct MemAllocRecord : _pi_object {
Expand Down
96 changes: 96 additions & 0 deletions sycl/plugins/unified_runtime/ur/ur.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
#pragma once

#include <atomic>
#include <cstdint>
#include <iostream>
#include <functional>
#include <mutex>
#include <shared_mutex>
#include <thread>
Expand Down Expand Up @@ -99,6 +101,100 @@ class SpinLock {
std::atomic_flag MLock = ATOMIC_FLAG_INIT;
};

// The wrapper for immutable data.
// The data is initialized only once at first access (via ->) with the
// initialization function provided in Init. All subsequent access to
// the data just returns the already stored data.
//
template <class T> struct ZeCache : private T {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be wrong, but Ze seems like a leftover from L0-related naming. Should it be renamed to be more general?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure, but renaming it here means re-naming uses in PI L0 plugin too, which might be big.
I'd prefer us to rename things that are completely moved to UR already.

// The initialization function takes a reference to the data
// it is going to initialize, since it is private here in
// order to disallow access other than through "->".
//
using InitFunctionType = std::function<void(T &)>;
InitFunctionType Compute{nullptr};
bool Computed{false};
pi_mutex ZeCacheMutex;

ZeCache() : T{} {}

// Access to the fields of the original T data structure.
T *operator->() {
std::unique_lock<pi_mutex> Lock(ZeCacheMutex);
if (!Computed) {
Compute(*this);
Computed = true;
}
return this;
}
};

// This wrapper around std::atomic is created to limit operations with reference
// counter and to make allowed operations more transparent in terms of
// thread-safety in the plugin. increment() and load() operations do not need a
// mutex guard around them since the underlying data is already atomic.
// decrementAndTest() method is used to guard a code which needs to be
// executed when object's ref count becomes zero after release. This method also
// doesn't need a mutex guard because decrement operation is atomic and only one
// thread can reach ref count equal to zero, i.e. only a single thread can pass
// through this check.
struct ReferenceCounter {
ReferenceCounter() : RefCount{1} {}

// Reset the counter to the initial value.
void reset() { RefCount = 1; }

// Used when retaining an object.
void increment() { RefCount++; }

// Supposed to be used in pi*GetInfo* methods where ref count value is
// requested.
uint32_t load() { return RefCount.load(); }

// This method allows to guard a code which needs to be executed when object's
// ref count becomes zero after release. It is important to notice that only a
// single thread can pass through this check. This is true because of several
// reasons:
// 1. Decrement operation is executed atomically.
// 2. It is not allowed to retain an object after its refcount reaches zero.
// 3. It is not allowed to release an object more times than the value of
// the ref count.
// 2. and 3. basically means that we can't use an object at all as soon as its
// refcount reaches zero. Using this check guarantees that code for deleting
// an object and releasing its resources is executed once by a single thread
// and we don't need to use any mutexes to guard access to this object in the
// scope after this check. Of course if we access another objects in this code
// (not the one which is being deleted) then access to these objects must be
// guarded, for example with a mutex.
bool decrementAndTest() { return --RefCount == 0; }

private:
std::atomic<uint32_t> RefCount;
};

// Base class to store common data
struct _pi_object {
_pi_object() : RefCount{} {}

// Must be atomic to prevent data race when incrementing/decrementing.
ReferenceCounter RefCount;

// This mutex protects accesses to all the non-const member variables.
// Exclusive access is required to modify any of these members.
//
// To get shared access to the object in a scope use std::shared_lock:
// std::shared_lock Lock(Obj->Mutex);
// To get exclusive access to the object in a scope use std::scoped_lock:
// std::scoped_lock Lock(Obj->Mutex);
//
// If several pi objects are accessed in a scope then each object's mutex must
// be locked. For example, to get write access to Obj1 and Obj2 and read
// access to Obj3 in a scope use the following approach:
// std::shared_lock Obj3Lock(Obj3->Mutex, std::defer_lock);
// std::scoped_lock LockAll(Obj1->Mutex, Obj2->Mutex, Obj3Lock);
pi_shared_mutex Mutex;
};

// Helper for one-liner validation
#define PI_ASSERT(condition, error) \
if (!(condition)) \
Expand Down