onnxruntime/onnxruntime/test/onnx/testcase_request.cc
Tim Harris 5e8952ef89
ThreadPool clean up : mm_pause in loops, correctly spin-then-wait, and adopt static methods consistently in the API (#5590)
Description: This change makes three changes to the ThreadPool class to clean up issues identified during performance analysis and optimization. (1) It uses mm_pause intrinsics in spin loops, helping avoid consuming pipeline resources while waiting. (2) It re-organizes the spin-then-steal loop for work distribution to start out spinning as intended, rather than to start out trying to steal. (3) It updates the ThreadPool class's API to be consistent in the use of static methods for public functions. The PR includes minor doc updates and corresponding changes to test cases.

Motivation and Context
The change helps ensure consistency in behavior between the OpenMP and Eigen-based implementations. Unlike the instance methods, the static methods abstract over the different ways in which threading can be implemented; they will map onto the OpenMP or Eigen-based implementations when threading is used. When threading is not used they will run work sequentially.
2020-10-28 09:49:18 +00:00

215 lines
8.2 KiB
C++

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "testcase_request.h"
#include "dataitem_request.h"
#include "TestCase.h"
#include "core/common/logging/logging.h"
#include "core/common/logging/macros.h"
#include <utility>
namespace onnxruntime {
namespace test {
TestCaseRequestContext::TestCaseRequestContext(const Callback& cb, PThreadPool tp, const ITestCase& test_case, Ort::Env& env,
const Ort::SessionOptions& session_opts, size_t test_case_id)
: cb_(cb),
tp_(tp),
test_case_(test_case),
env_(env),
session_opts_(session_opts.Clone()),
session_(nullptr),
test_case_id_(test_case_id),
allocator_(),
result_(),
data_tasks_started_(0),
data_tasks_inprogress_(0) {
result_ = std::make_shared<TestCaseResult>(test_case_.GetDataCount(), EXECUTE_RESULT::NOT_SET, test_case_.GetTestCaseName());
SetTimeSpecToZero(&test_case_time_);
CallableFactory<TestCaseRequestContext, void, size_t, EXECUTE_RESULT, const TIME_SPEC&> f(this);
on_data_task_cb_ = f.GetCallable<&TestCaseRequestContext::OnDataTaskComplete>();
}
bool TestCaseRequestContext::SetupSession() {
ORT_TRY {
const auto* test_case_name = test_case_.GetTestCaseName().c_str();
session_opts_.SetLogId(test_case_name);
Ort::Session session{env_, test_case_.GetModelUrl(), session_opts_};
session_ = std::move(session);
LOGF_DEFAULT(INFO, "Testing %s\n", test_case_name);
return true;
}
ORT_CATCH(const Ort::Exception& ex) {
ORT_HANDLE_EXCEPTION([&]() {
LOGF_DEFAULT(ERROR, "Model %s failed to load:%s", test_case_.GetTestCaseName().c_str(), ex.what());
result_ = std::make_shared<TestCaseResult>(test_case_.GetDataCount(), EXECUTE_RESULT::NOT_SUPPORT, "");
});
}
return false;
}
std::shared_ptr<TestCaseResult> TestCaseRequestContext::Run(PThreadPool tpool,
const ITestCase& c, Ort::Env& env,
const Ort::SessionOptions& session_opts,
size_t concurrent_runs, size_t repeat_count) {
//temp hack. Because we have no resource control. We may not have enough memory to run this test in parallel
if (c.GetTestCaseName() == "coreml_FNS-Candy_ImageNet") {
concurrent_runs = 1;
}
// No callback, test_case_id is zero.
Callback empty_cb;
TestCaseRequestContext ctx(empty_cb, tpool, c, env, session_opts, 0U);
const size_t data_count = c.GetDataCount();
if (concurrent_runs > 1 && data_count > 1) {
ctx.RunAsync(concurrent_runs);
ctx.Wait();
} else {
ctx.RunSequentially(repeat_count);
}
auto result = ctx.GetResult();
return result;
}
void TestCaseRequestContext::Request(const Callback& cb, PThreadPool tpool,
const ITestCase& c,
Ort::Env& env,
const Ort::SessionOptions& session_opts,
size_t test_case_id,
size_t concurrent_runs) {
//temp hack. Because we have no resource control. We may not have enough memory to run this test in parallel
if (c.GetTestCaseName() == "coreml_FNS-Candy_ImageNet") {
concurrent_runs = 1;
}
std::unique_ptr<TestCaseRequestContext> self(new TestCaseRequestContext(cb, tpool, c, env, session_opts, test_case_id));
CallableFactory<TestCaseRequestContext, void, size_t> f(self.get());
auto runnable = f.GetCallable<&TestCaseRequestContext::RunAsync>();
onnxruntime::concurrency::ThreadPool::Schedule(tpool, [runnable, concurrent_runs]() { runnable.Invoke(concurrent_runs); });
self.release();
}
void TestCaseRequestContext::RunAsync(size_t concurrent_runs) {
assert(concurrent_runs > 0);
if (!SetupSession()) {
return OnTestCaseComplete();
}
concurrent_runs = std::min(concurrent_runs, test_case_.GetDataCount());
// Reserve one refcount for this thread so the object does not get deleted when
// several TestCases are run in parallel
// by worker threads before this thread finishes. In exchange, we run one of the tasks.
auto this_task_id = data_tasks_started_.fetch_add(1, std::memory_order_relaxed);
data_tasks_inprogress_.fetch_add(1, std::memory_order_relaxed);
for (size_t i = 1; i < concurrent_runs; ++i) {
auto next_to_run = data_tasks_started_.fetch_add(1, std::memory_order_relaxed);
if (next_to_run >= test_case_.GetDataCount()) {
break;
}
data_tasks_inprogress_.fetch_add(1, std::memory_order_relaxed);
DataTaskRequestContext::Request(on_data_task_cb_, tp_, test_case_, session_, &allocator_, next_to_run);
}
// This runs in this thread and we should invoke the callback for it.
auto result = DataTaskRequestContext::Run(test_case_, session_, &allocator_, this_task_id);
OnDataTaskComplete(this_task_id, result.first, result.second);
}
void TestCaseRequestContext::OnDataTaskComplete(size_t task_id, EXECUTE_RESULT result, const TIME_SPEC& spent_time) {
TIME_SPEC zero;
SetTimeSpecToZero(&zero);
AccumulateTimeSpec(&test_case_time_, &zero, &spent_time);
result_->SetResult(task_id, result);
auto next_to_run = data_tasks_started_.fetch_add(1, std::memory_order_relaxed);
if (next_to_run < test_case_.GetDataCount()) {
data_tasks_inprogress_.fetch_add(1, std::memory_order_relaxed);
DataTaskRequestContext::Request(on_data_task_cb_, tp_, test_case_, session_, &allocator_, next_to_run);
}
auto before_we_done = data_tasks_inprogress_.fetch_sub(1, std::memory_order_acq_rel);
assert(before_we_done > 0);
if (before_we_done == 1U) {
CalculateAndLogStats();
OnTestCaseComplete();
}
}
void TestCaseRequestContext::OnTestCaseComplete() {
if (cb_) {
std::unique_ptr<TestCaseRequestContext> self(this);
cb_.Invoke(test_case_id_, std::move(result_));
// No member access beyond this point
} else {
std::lock_guard<std::mutex> g(mut_);
finished_ = true;
// We do not unlock here before notifying
// so the Waiting thread does not destroy us before
// we access cond_ in case it discovers finished_ is already true
cond_.notify_one();
}
}
void TestCaseRequestContext::RunSequentially(size_t repeat_count) {
if (!SetupSession()) {
return;
}
TIME_SPEC zero;
SetTimeSpecToZero(&zero);
const size_t data_count = test_case_.GetDataCount();
for (size_t idx_repeat = 0; idx_repeat < repeat_count; ++idx_repeat) {
for (size_t idx_data = 0; idx_data != data_count; ++idx_data) {
auto result = DataTaskRequestContext::Run(test_case_, session_, &allocator_, idx_data);
result_->SetResult(idx_data, result.first);
AccumulateTimeSpec(&test_case_time_, &zero, &result.second);
}
}
CalculateAndLogStats();
}
void TestCaseRequestContext::Wait() const {
std::unique_lock<std::mutex> ul(mut_);
while (!finished_) {
cond_.wait(ul);
}
}
void TestCaseRequestContext::CalculateAndLogStats() const {
result_->SetSpentTime(test_case_time_);
const auto& test_case_name = test_case_.GetTestCaseName();
const std::vector<EXECUTE_RESULT>& er = result_->GetExcutionResult();
for (size_t i = 0; i != er.size(); ++i) {
EXECUTE_RESULT r = er[i];
if (r == EXECUTE_RESULT::SUCCESS) continue;
std::string s = test_case_.GetDatasetDebugInfoString(i);
switch (r) {
case EXECUTE_RESULT::RESULT_DIFFERS:
LOGF_DEFAULT(ERROR, "%s: result differs. Dataset:%s\n", test_case_name.c_str(), s.c_str());
break;
case EXECUTE_RESULT::SHAPE_MISMATCH:
LOGF_DEFAULT(ERROR, "%s: shape mismatch. Dataset:%s\n", test_case_name.c_str(), s.c_str());
break;
case EXECUTE_RESULT::TYPE_MISMATCH:
LOGF_DEFAULT(ERROR, "%s: type mismatch. Dataset:%s\n", test_case_name.c_str(), s.c_str());
break;
case EXECUTE_RESULT::MODEL_SHAPE_MISMATCH:
LOGF_DEFAULT(ERROR, "%s: shape in model file mismatch. Dataset:%s\n", test_case_name.c_str(), s.c_str());
break;
case EXECUTE_RESULT::MODEL_TYPE_MISMATCH:
LOGF_DEFAULT(ERROR, "%s: type in model file mismatch. Dataset:%s\n", test_case_name.c_str(), s.c_str());
default:
//nothing to do
break;
}
break;
}
}
} // namespace test
} // namespace onnxruntime