Skip to content
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

Added mutex locks in register_target.cpp and created a multithreading… #2224

Closed
wants to merge 13 commits into from
65 changes: 56 additions & 9 deletions src/register_target.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 Advanced Micro Devices, Inc. All rights reserved.
* Copyright (c) 2015-2023 Advanced Micro Devices, Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand All @@ -21,6 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <mutex>
#include <string>
#include <unordered_map>
#include <migraphx/register_target.hpp>
Expand All @@ -33,42 +34,88 @@ inline namespace MIGRAPHX_INLINE_NS {
void store_target_lib(const dynamic_loader& lib)
{
static std::vector<dynamic_loader> target_loader;
static std::mutex mutex;
std::unique_lock<std::mutex> lock(mutex);

target_loader.emplace_back(lib);
}

/**
* Returns a singleton map of targets and names.
*/
std::unordered_map<std::string, target>& target_map()
{
static std::unordered_map<std::string, target> m; // NOLINT
return m;
}

/**
* Returns a singleton mutex used by the various register_target methods.
*/
std::mutex& target_mutex()
{
static std::mutex m; // NOLINT
return m;
}

void register_target_init() { (void)target_map(); }

void unregister_target(const std::string& name)
{
std::unique_lock<std::mutex> lock(target_mutex());
assert(target_map().count(name));
target_map().erase(name);
}

void register_target(const target& t) { target_map()[t.name()] = t; }
/**
* Insert a target name in the target_map; thread safe.
*/
void register_target(const target& t)
{
std::unique_lock<std::mutex> lock(target_mutex());
target_map()[t.name()] = t;
Copy link
Collaborator

Choose a reason for hiding this comment

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

You should be unlocking here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With std::unique_lock, the destructor automatically unlocks it when it goes out of scope. This is supposed not only to be a coding convenience, but gives protection when things are interrupted by an exception. From cplusplus.com:
"This class guarantees an unlocked status on destruction (even if not called explicitly). Therefore it is especially useful as an object with automatic duration, as it guarantees the mutex object is properly unlocked in case an exception is thrown."

}

/**
* Search for a target by name in the target_map; thread-safe.
*/
migraphx::optional<target> find_target(const std::string& name)
{
// search for match or return none
std::unique_lock<std::mutex> lock(target_mutex());
const auto it = target_map().find(name);

Copy link
Collaborator

Choose a reason for hiding this comment

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

Add unlock before each return

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't need to--see above

if(it == target_map().end())
return nullopt;
return it->second;
}

/**
* Get a target by name. Load target library and register target if needed.
* Thread safe.
*/
target make_target(const std::string& name)
{
if(not contains(target_map(), name))
// no lock required here
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we not concerned with something trying to reregister a target in another thread? If between find_target, we end up doing an additional find()

Is the intent to not expose find_target() outside of register_target? If so, why not just add the locking around here.

Copy link
Contributor Author

@bpickrel bpickrel Oct 3, 2023

Choose a reason for hiding this comment

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

The idea is that find_target does contain a lock, so make_target is already protected. The second thread would have to wait for the first thread to finish before trying to register it, then see that it's already there.

auto t = find_target(name);
if(t == nullopt)
{
std::string target_name = "libmigraphx_" + name + ".so";
// register_target is called by this
store_target_lib(dynamic_loader(target_name));
t = find_target(name);
}
const auto it = target_map().find(name);
if(it == target_map().end())
{
MIGRAPHX_THROW("Requested target '" + name + "' is not loaded or not supported");
}
return it->second;
// at this point we should always have a target
Copy link
Member

@umangyadav umangyadav Oct 3, 2023

Choose a reason for hiding this comment

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

Why is it guaranteed to have a target at this point?
Let's say Target requested is not supported by MIGraphX e.g. some target named "TPU". TPU is not registered with MIGraphX and it will not be found or can not be loaded dynamically either.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agreed, we should check if (t == nullopt) a second time and throw.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is it guaranteed to have a target at this point? Let's say Target requested is not supported by MIGraphX e.g. some target named "TPU". TPU is not registered with MIGraphX and it will not be found or can not be loaded dynamically either.

Because the dynamic_loader constructor will throw an exception. I added a line in the test for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, we should check if (t == nullopt) a second time and throw.

That's redundant since find_target contains that check.


return *t;
}

/**
* Get list of names of registered targets.
*/
std::vector<std::string> get_targets()
{
std::unique_lock<std::mutex> lock(target_mutex());
std::vector<std::string> result;
std::transform(target_map().begin(),
target_map().end(),
Expand Down
40 changes: 40 additions & 0 deletions test/targets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
#include <migraphx/register_target.hpp>
#include <migraphx/target.hpp>
#include <thread>
#include "test.hpp"

TEST_CASE(make_target)
Expand Down Expand Up @@ -50,4 +51,43 @@ TEST_CASE(targets)
EXPECT(ts.size() >= 1);
}

TEST_CASE(concurrent_targets)
bpickrel marked this conversation as resolved.
Show resolved Hide resolved
{
std::vector<std::thread> threads;
#ifdef HAVE_GPU
std::string target_name = "gpu";
#elif defined(HAVE_CPU)
std::string target_name = "cpu";
#elif defined(HAVE_FPGA)
std::string target_name = "fpga";
#else
std::string target_name = "ref";
#endif

auto n_threads = std::thread::hardware_concurrency() * 4;

for(auto i = 0u; i < n_threads; i++)
{
auto thread_body = [&target_name]() {
// TODO: remove all existing targets, if any.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Followup: I disabled this extra test but when I tested the existing fixes with amdinfer, the Inference Server, the segmentation violation still occurs. I'm going to recommend we commit this PR as is but continue to search for the root cause of Issue #2208.

// The existing code cannot pass a test in which different threads
// register and unregister the same targets; not known if this is
// needed in any deployed product.
// std::vector<std::string> target_list = migraphx::get_targets();
// for(const auto& tt : target_list)
// migraphx::unregister_target(tt);

auto ref_target = migraphx::make_target(target_name);
migraphx::register_target(ref_target);
bpickrel marked this conversation as resolved.
Show resolved Hide resolved
EXPECT(test::throws([&] { ref_target = migraphx::make_target("xyz"); }));

migraphx::get_targets();
};

threads.emplace_back(thread_body);
}
for(auto& tt : threads)
tt.join();
bpickrel marked this conversation as resolved.
Show resolved Hide resolved
}

int main(int argc, const char* argv[]) { test::run(argc, argv); }
Loading