#include <numeric>
#include <thread>
#include "cache_manager.h"
#include "gtest/gtest-spi.h"
#include "gtest/gtest.h"
#include "memory.h"
#include "triton/common/logging.h"
namespace tc = triton::core;
namespace triton { namespace core {
Status
InferenceResponseFactory::CreateResponse(
std::unique_ptr<InferenceResponse>* response) const
{
response->reset(new InferenceResponse(
model_, id_, allocator_, alloc_userp_, response_fn_, response_userp_,
response_delegator_));
return Status::Success;
}
InferenceRequest::InferenceRequest(
Model* model, const int64_t requested_model_version)
: needs_normalization_(true), model_raw_(model),
requested_model_version_(requested_model_version), flags_(0),
correlation_id_(0), batch_size_(0), timeout_us_(0), collect_stats_(true)
{
response_factory_.reset(new InferenceResponseFactory());
}
InferenceRequest::Input::Input(
const std::string& name, const inference::DataType datatype,
const int64_t* shape, const uint64_t dim_count)
: name_(name), datatype_(datatype),
original_shape_(shape, shape + dim_count),
tensor_type_(TensorType::TENSOR), data_(new MemoryReference),
has_host_policy_specific_data_(false)
{
}
const std::string MODEL = "model";
const std::string&
InferenceRequest::ModelName() const
{
return MODEL;
}
int64_t
InferenceRequest::ActualModelVersion() const
{
return requested_model_version_;
}
Status
InferenceRequest::PrepareForInference()
{
inputs_.clear();
override_inputs_.clear();
for (auto& pr : original_inputs_) {
inputs_.emplace(std::make_pair(pr.first, std::addressof(pr.second)));
}
queue_start_ns_ = 0;
#ifdef TRITON_ENABLE_STATS
request_start_ns_ = 0;
#endif
return Status::Success;
}
Status
InferenceRequest::Input::DataBuffer(
const size_t idx, const void** base, size_t* byte_size,
TRITONSERVER_MemoryType* memory_type, int64_t* memory_type_id) const
{
*base = data_->BufferAt(idx, byte_size, memory_type, memory_type_id);
return Status::Success;
}
Status
InferenceRequest::AddOriginalInput(
const std::string& name, const inference::DataType datatype,
const int64_t* shape, const uint64_t dim_count,
InferenceRequest::Input** input)
{
const auto& pr = original_inputs_.emplace(
std::piecewise_construct, std::forward_as_tuple(name),
std::forward_as_tuple(name, datatype, shape, dim_count));
if (!pr.second) {
return Status(
Status::Code::INVALID_ARG,
"input '" + name + "' already exists in request");
}
if (input != nullptr) {
*input = std::addressof(pr.first->second);
}
needs_normalization_ = true;
return Status::Success;
}
Status
InferenceRequest::AddOriginalInput(
const std::string& name, const inference::DataType datatype,
const std::vector<int64_t>& shape, InferenceRequest::Input** input)
{
return AddOriginalInput(name, datatype, &shape[0], shape.size(), input);
}
Status
InferenceRequest::Input::AppendData(
const void* base, size_t byte_size, TRITONSERVER_MemoryType memory_type,
int64_t memory_type_id)
{
if (byte_size > 0) {
std::static_pointer_cast<MemoryReference>(data_)->AddBuffer(
static_cast<const char*>(base), byte_size, memory_type, memory_type_id);
}
return Status::Success;
}
InferenceResponse::InferenceResponse(
const std::shared_ptr<Model>& model, const std::string& id,
const ResponseAllocator* allocator, void* alloc_userp,
TRITONSERVER_InferenceResponseCompleteFn_t response_fn,
void* response_userp,
const std::function<
void(std::unique_ptr<InferenceResponse>&&, const uint32_t)>& delegator)
: model_(model), id_(id), allocator_(allocator), alloc_userp_(alloc_userp),
response_fn_(response_fn), response_userp_(response_userp),
response_delegator_(delegator), null_response_(false)
{
}
std::ostream&
operator<<(std::ostream& out, const InferenceResponse& response)
{
out << "[0x" << std::addressof(response) << "] "
<< "response id: " << response.Id() << std::endl;
out << "status:" << response.ResponseStatus().AsString() << std::endl;
return out;
}
InferenceResponse::Output::~Output()
{
Status status = ReleaseDataBuffer();
if (!status.IsOk()) {
std::cerr << "[ERROR] failed to release buffer for output '" << name_
<< "': " << status.AsString();
}
}
Status
InferenceResponse::Output::ReleaseDataBuffer()
{
if (allocated_buffer_ != nullptr) {
free(allocated_buffer_);
}
allocated_buffer_ = nullptr;
buffer_attributes_.SetByteSize(0);
buffer_attributes_.SetMemoryType(TRITONSERVER_MEMORY_CPU);
buffer_attributes_.SetMemoryTypeId(0);
allocated_userp_ = nullptr;
return Status::Success;
}
Status
InferenceResponse::Output::DataBuffer(
const void** buffer, size_t* buffer_byte_size,
TRITONSERVER_MemoryType* memory_type, int64_t* memory_type_id,
void** userp) const
{
*buffer = allocated_buffer_;
*buffer_byte_size = buffer_attributes_.ByteSize();
*memory_type = buffer_attributes_.MemoryType();
*memory_type_id = buffer_attributes_.MemoryTypeId();
*userp = allocated_userp_;
return Status::Success;
}
Status
InferenceResponse::Output::AllocateDataBuffer(
void** buffer, size_t buffer_byte_size,
TRITONSERVER_MemoryType* memory_type, int64_t* memory_type_id)
{
if (allocated_buffer_ != nullptr) {
return Status(
Status::Code::ALREADY_EXISTS,
"allocated buffer for output '" + name_ + "' already exists");
}
if (*memory_type != TRITONSERVER_MEMORY_CPU || *memory_type_id != 0) {
return Status(
Status::Code::INTERNAL, "Only standard CPU memory supported for now");
}
*buffer = malloc(buffer_byte_size);
if (buffer == nullptr || *buffer == nullptr) {
return Status(
Status::Code::INTERNAL, "buffer was nullptr in AllocateDataBuffer");
}
allocated_buffer_ = *buffer;
buffer_attributes_.SetByteSize(buffer_byte_size);
buffer_attributes_.SetMemoryType(*memory_type);
buffer_attributes_.SetMemoryTypeId(*memory_type_id);
allocated_userp_ = nullptr;
return Status::Success;
}
Status
InferenceResponse::AddOutput(
const std::string& name, const inference::DataType datatype,
const std::vector<int64_t>& shape, InferenceResponse::Output** output)
{
outputs_.emplace_back(name, datatype, shape, allocator_, alloc_userp_);
if (output != nullptr) {
*output = std::addressof(outputs_.back());
}
return Status::Success;
}
InferenceRequest::SequenceId::SequenceId()
: sequence_label_(""), sequence_index_(0),
id_type_(InferenceRequest::SequenceId::DataType::UINT64)
{
}
InferenceRequest::SequenceId::SequenceId(const std::string& sequence_label)
: sequence_label_(sequence_label), sequence_index_(0),
id_type_(InferenceRequest::SequenceId::DataType::STRING)
{
}
InferenceRequest::SequenceId::SequenceId(uint64_t sequence_index)
: sequence_label_(""), sequence_index_(sequence_index),
id_type_(InferenceRequest::SequenceId::DataType::UINT64)
{
}
}}
namespace helpers {
void
CheckStatus(tc::Status status)
{
ASSERT_TRUE(status.IsOk()) << "ERROR: " << status.Message();
}
void
InsertWrapper(
std::shared_ptr<tc::TritonCache> cache, tc::InferenceResponse* r,
std::string key)
{
CheckStatus(cache->Insert(r, key));
}
void
LookupWrapper(
std::shared_ptr<tc::TritonCache> cache, tc::InferenceResponse* r,
std::string key)
{
CheckStatus(cache->Lookup(r, key));
}
void
LookupWrapperMaybeMiss(
std::shared_ptr<tc::TritonCache> cache, tc::InferenceResponse* r,
std::string key)
{
auto status = cache->Lookup(r, key);
auto ok =
(status.IsOk() || status.StatusCode() == tc::Status::Code::NOT_FOUND);
ASSERT_TRUE(ok) << "ERROR: " << status.Message();
}
void
reset_response(
std::unique_ptr<tc::InferenceResponse>* response,
tc::InferenceRequest* request)
{
helpers::CheckStatus(request->ResponseFactory()->CreateResponse(response));
}
struct Tensor {
std::string name;
std::vector<int> data;
};
std::unique_ptr<tc::InferenceResponse>
GenerateResponse(
const tc::InferenceRequest* request, inference::DataType dtype,
TRITONSERVER_MemoryType memory_type, int64_t memory_type_id,
const std::vector<helpers::Tensor>& outputs)
{
std::cout << "Create response object" << std::endl;
std::unique_ptr<tc::InferenceResponse> response;
helpers::CheckStatus(request->ResponseFactory()->CreateResponse(&response));
std::cout << "Add output metadata to response object" << std::endl;
for (const auto& tensor : outputs) {
if (tensor.data.size() == 0) {
std::cout << "[ERROR] Can't generate a request with no output data"
<< std::endl;
return nullptr;
}
tc::InferenceResponse::Output* response_output = nullptr;
std::vector<int64_t> shape{1, -1};
shape[1] = tensor.data.size();
uint64_t output_size = sizeof(tensor.data[0]) * tensor.data.size();
helpers::CheckStatus(
response->AddOutput(tensor.name, dtype, shape, &response_output));
std::cout << "Allocate output data buffer for response object of size: "
<< output_size << std::endl;
void* buffer;
helpers::CheckStatus(response_output->AllocateDataBuffer(
&buffer, output_size, &memory_type, &memory_type_id));
if (buffer == nullptr) {
std::cout << "[ERROR] buffer was nullptr;" << std::endl;
return nullptr;
}
std::memcpy(buffer, tensor.data.data(), output_size);
}
return response;
}
tc::InferenceRequest*
GenerateRequest(
tc::Model* model, uint64_t model_version, inference::DataType dtype,
TRITONSERVER_MemoryType memory_type, int64_t memory_type_id,
const std::vector<helpers::Tensor>& inputs, const std::string& request_id)
{
auto request = new tc::InferenceRequest(model, model_version);
for (const auto& tensor : inputs) {
if (tensor.data.size() == 0) {
std::cout << "[ERROR] Can't generate a request with no input data"
<< std::endl;
return nullptr;
}
tc::InferenceRequest::Input* request_input = nullptr;
std::vector<int64_t> shape{1, -1};
shape[1] = tensor.data.size();
request->AddOriginalInput(tensor.name, dtype, shape, &request_input);
if (request_input == nullptr) {
std::cout << "[ERROR] request_input was nullptr" << std::endl;
return nullptr;
}
uint64_t input_size = sizeof(tensor.data[0]) * tensor.data.size();
request_input->AppendData(
tensor.data.data(), input_size, memory_type, memory_type_id);
}
helpers::CheckStatus(request->PrepareForInference());
request->SetId(request_id); return request;
}
tc::Status
InsertLookupCompare(
std::shared_ptr<tc::TritonCache> cache,
std::vector<boost::span<tc::Byte>> expected_buffers, const std::string& key)
{
if (!cache) {
return tc::Status(tc::Status::Code::INTERNAL, "cache was nullptr");
} else if (expected_buffers.empty()) {
return tc::Status(tc::Status::Code::INTERNAL, "entry was empty");
}
helpers::CheckStatus(cache->Insert(expected_buffers, key));
auto lookup_entry = std::make_unique<tc::CacheEntry>();
auto status = cache->Lookup(key, lookup_entry.get());
if (!status.IsOk()) {
return tc::Status(
tc::Status::Code::INTERNAL, "Lookup failed: " + status.Message());
}
auto lookup_buffers = lookup_entry->Buffers();
if (lookup_buffers.size() != expected_buffers.size()) {
return tc::Status(
tc::Status::Code::INTERNAL,
"Expected " + std::to_string(expected_buffers.size()) + " got " +
std::to_string(lookup_buffers.size()));
}
for (size_t b = 0; b < expected_buffers.size(); b++) {
boost::span<tc::Byte> lookup = {
static_cast<tc::Byte*>(lookup_buffers[b].first),
lookup_buffers[b].second};
boost::span<tc::Byte> expected = expected_buffers[b];
if (!std::equal(
lookup.begin(), lookup.end(), expected.begin(), expected.end())) {
return tc::Status(
tc::Status::Code::INTERNAL,
"Buffer bytes didn't match for test input");
}
}
return tc::Status::Success;
}
std::shared_ptr<tc::TritonCache>
CreateLocalCache(uint64_t cache_size)
{
std::shared_ptr<tc::TritonCacheManager> cache_manager;
auto cache_dir = "/opt/tritonserver/caches";
helpers::CheckStatus(
tc::TritonCacheManager::Create(&cache_manager, cache_dir));
std::shared_ptr<tc::TritonCache> cache;
auto cache_config = R"({"size": )" + std::to_string(cache_size) + "}";
std::cout << "Creating local cache with config: " << cache_config
<< std::endl;
auto cache_name = "local";
helpers::CheckStatus(
cache_manager->CreateCache(cache_name, cache_config, &cache));
return cache;
}
std::shared_ptr<tc::TritonCache>
CreateRedisCache(std::string host, std::string port)
{
std::shared_ptr<tc::TritonCacheManager> cache_manager;
auto cache_dir = "/opt/tritonserver/caches";
helpers::CheckStatus(
tc::TritonCacheManager::Create(&cache_manager, cache_dir));
std::shared_ptr<tc::TritonCache> cache;
std::ostringstream cache_config_json;
auto cache_config =
R"({"host": ")" + host + R"(", "port": ")" + port + R"("})";
std::cout << "Creating redis cache with config: " << cache_config
<< std::endl;
auto cache_name = "redis";
helpers::CheckStatus(
cache_manager->CreateCache(cache_name, cache_config, &cache));
return cache;
}
void
CreateCacheExpectFail(
const std::string& cache_name, const std::string& cache_config)
{
std::shared_ptr<tc::TritonCacheManager> cache_manager;
auto cache_dir = "/opt/tritonserver/caches";
helpers::CheckStatus(
tc::TritonCacheManager::Create(&cache_manager, cache_dir));
std::shared_ptr<tc::TritonCache> cache;
auto status = cache_manager->CreateCache(cache_name, cache_config, &cache);
ASSERT_FALSE(status.IsOk()) << "Creating cache with config: '" << cache_config
<< "' succeeded when it should fail.";
ASSERT_EQ(cache, nullptr);
}
}
namespace {
class RequestResponseCacheTest : public ::testing::Test {
protected:
void SetUp() override
{
data0 = {1, 2, 3, 4};
data1 = {5, 6, 7, 8};
inputs0 = std::vector<helpers::Tensor>{{"input", data0}};
inputs1 = std::vector<helpers::Tensor>{{"input", data1}};
inputs2 = std::vector<helpers::Tensor>{{"input", data1}};
inputs3 =
std::vector<helpers::Tensor>{{"input0", data0}, {"input1", data1}};
inputs4 =
std::vector<helpers::Tensor>{{"input1", data1}, {"input0", data0}};
request0 = helpers::GenerateRequest(
model, model_version, dtype, memory_type, memory_type_id, inputs0,
"request0");
request1 = helpers::GenerateRequest(
model, model_version, dtype, memory_type, memory_type_id, inputs1,
"request1");
request2 = helpers::GenerateRequest(
model, model_version, dtype, memory_type, memory_type_id, inputs2,
"request2");
request3 = helpers::GenerateRequest(
model, model_version, dtype, memory_type, memory_type_id, inputs3,
"request3");
request4 = helpers::GenerateRequest(
model, model_version, dtype, memory_type, memory_type_id, inputs4,
"request4");
ASSERT_NE(request0, nullptr);
ASSERT_NE(request1, nullptr);
ASSERT_NE(request2, nullptr);
ASSERT_NE(request3, nullptr);
ASSERT_NE(request4, nullptr);
for (size_t idx = 0; idx < thread_count; idx++) {
std::vector<int> data(thread_count, static_cast<int>(idx));
std::vector<helpers::Tensor> inputs{
helpers::Tensor{"input" + std::to_string(idx), data}};
std::string request_id = "unique" + std::to_string(idx);
auto request = helpers::GenerateRequest(
model, model_version, dtype, memory_type, memory_type_id, inputs,
request_id);
ASSERT_NE(request, nullptr);
unique_requests.emplace_back(request);
}
ASSERT_EQ(unique_requests.size(), thread_count);
helpers::Tensor output_tensor0 = {"output", data0};
outputs0 = std::vector<helpers::Tensor>{output_tensor0};
data100 = std::vector<int>(100, 0);
std::iota(data100.begin(), data100.end(), 1);
output100_size = sizeof(int) * data100.size();
helpers::Tensor output_tensor100 = {"output", data100};
outputs100 = std::vector<helpers::Tensor>{output_tensor100};
response0 = helpers::GenerateResponse(
request0, dtype, memory_type, memory_type_id, outputs0);
ASSERT_NE(response0, nullptr);
response_400bytes = helpers::GenerateResponse(
request0, dtype, memory_type, memory_type_id, outputs100);
ASSERT_NE(response_400bytes, nullptr);
auto rh = std::getenv("TRITON_REDIS_HOST");
if (rh) {
redis_host = rh;
}
auto rp = std::getenv("TRITON_REDIS_PORT");
if (rp) {
redis_port = rp;
}
}
void TearDown() override
{
delete request0;
delete request1;
delete request2;
delete request3;
delete request4;
for (auto r : unique_requests) {
delete r;
}
}
public:
tc::Model* model = nullptr;
uint64_t model_version = 1;
inference::DataType dtype = inference::DataType::TYPE_INT32;
TRITONSERVER_MemoryType memory_type = TRITONSERVER_MEMORY_CPU;
int64_t memory_type_id = 0;
size_t thread_count = 100;
uint64_t output100_size;
std::string redis_host = "localhost";
std::string redis_port = "6379";
std::vector<int> data0, data1, data100;
std::vector<helpers::Tensor> inputs0, inputs1, inputs2, inputs3, inputs4,
inputs100;
std::vector<helpers::Tensor> outputs0, outputs100;
tc::InferenceRequest *request0, *request1, *request2, *request3, *request4;
std::vector<tc::InferenceRequest*> unique_requests;
std::unique_ptr<tc::InferenceResponse> response0, response_400bytes;
};
namespace tests {
void
InsertLookupCompareBytes(std::shared_ptr<tc::TritonCache> cache)
{
std::vector<tc::Byte> buffer1{1, tc::Byte{1}};
std::vector<tc::Byte> buffer2{2, tc::Byte{2}};
std::vector<tc::Byte> buffer3{4, tc::Byte{4}};
std::vector<tc::Byte> buffer4{8, tc::Byte{8}};
std::vector<tc::Byte> buffer5{16, tc::Byte{16}};
std::vector<boost::span<tc::Byte>> entry;
entry.push_back(buffer1);
entry.push_back(buffer2);
entry.push_back(buffer3);
entry.push_back(buffer4);
entry.push_back(buffer5);
helpers::CheckStatus(
helpers::InsertLookupCompare(cache, entry, "TestCacheEntry"));
}
void
HashUnique(
std::shared_ptr<tc::TritonCache> cache,
std::vector<tc::InferenceRequest*>& unique_requests)
{
ASSERT_NE(unique_requests.size(), 0);
std::vector<std::string> hashes;
for (const auto& request : unique_requests) {
std::string hash = "";
helpers::CheckStatus(cache->Hash(*request, &hash));
ASSERT_NE(hash, "");
hashes.push_back(hash);
}
ASSERT_NE(hashes.size(), 0);
for (size_t i = 0; i < hashes.size(); i++) {
for (size_t j = 0; j < hashes.size(); j++) {
if (i == j) {
continue;
}
ASSERT_NE(hashes[i], hashes[j]);
}
}
}
void
HashLogic(
std::shared_ptr<tc::TritonCache> cache, tc::InferenceRequest* request0,
tc::InferenceRequest* request1, tc::InferenceRequest* request2,
tc::InferenceRequest* request3, tc::InferenceRequest* request4)
{
std::string hash0, hash1, hash2, hash3, hash4;
helpers::CheckStatus(cache->Hash(*request0, &hash0));
helpers::CheckStatus(cache->Hash(*request1, &hash1));
helpers::CheckStatus(cache->Hash(*request2, &hash2));
helpers::CheckStatus(cache->Hash(*request3, &hash3));
helpers::CheckStatus(cache->Hash(*request4, &hash4));
ASSERT_NE(hash0, hash1);
ASSERT_EQ(hash1, hash2);
ASSERT_EQ(hash3, hash4);
}
void
ParallelInsert(
std::shared_ptr<tc::TritonCache> cache, size_t thread_count,
std::unique_ptr<tc::InferenceResponse>& insert_response,
size_t expected_cache_hits)
{
std::vector<std::thread> threads;
std::cout << "Insert responses into cache with [" << thread_count
<< "] threads in parallel" << std::endl;
for (size_t idx = 0; idx < thread_count; idx++) {
auto key = std::to_string(idx);
threads.emplace_back(std::thread(
&helpers::InsertWrapper, cache, insert_response.get(), key));
}
for (size_t idx = 0; idx < thread_count; idx++) {
threads[idx].join();
}
size_t cache_hits = 0;
size_t cache_misses = 0;
for (size_t idx = 0; idx < thread_count; idx++) {
auto key = std::to_string(idx);
auto entry = std::make_unique<tc::CacheEntry>();
auto status = cache->Lookup(key, entry.get());
if (status.IsOk()) {
cache_hits++;
} else {
cache_misses++;
}
}
ASSERT_EQ(cache_hits, expected_cache_hits);
ASSERT_EQ(cache_hits + cache_misses, thread_count);
}
void
ParallelLookup(
std::shared_ptr<tc::TritonCache> cache, size_t thread_count,
std::unique_ptr<tc::InferenceResponse>& insert_response,
std::vector<tc::InferenceRequest*>& unique_requests,
std::vector<int> expected_outputs)
{
const size_t expected_cache_hits = thread_count;
constexpr size_t expected_cache_misses = 0;
std::vector<std::thread> threads;
std::vector<std::unique_ptr<tc::InferenceResponse>> responses;
for (size_t idx = 0; idx < thread_count; idx++) {
std::unique_ptr<tc::InferenceResponse> response;
helpers::CheckStatus(
unique_requests[idx]->ResponseFactory()->CreateResponse(&response));
responses.push_back(std::move(response));
auto key = std::to_string(idx);
cache->Insert(insert_response.get(), key);
}
size_t cache_hits = 0;
size_t cache_misses = 0;
for (size_t idx = 0; idx < thread_count; idx++) {
auto key = std::to_string(idx);
auto entry = std::make_unique<tc::CacheEntry>();
auto status = cache->Lookup(key, entry.get());
if (status.IsOk()) {
cache_hits++;
} else {
std::cout << "ERROR: " << status.Message() << std::endl;
cache_misses++;
}
}
ASSERT_EQ(cache_hits, expected_cache_hits);
ASSERT_EQ(cache_misses, expected_cache_misses);
ASSERT_EQ(cache_hits + cache_misses, thread_count);
std::cout << "Lookup from cache with [" << thread_count
<< "] threads in parallel" << std::endl;
for (size_t idx = 0; idx < thread_count; idx++) {
auto key = std::to_string(idx);
threads.emplace_back(
std::thread(&helpers::LookupWrapper, cache, responses[idx].get(), key));
}
for (size_t idx = 0; idx < thread_count; idx++) {
threads[idx].join();
}
const auto& response0_output = insert_response->Outputs()[0];
for (size_t idx = 0; idx < thread_count; idx++) {
const void* response_buffer = nullptr;
size_t response_byte_size = 0;
TRITONSERVER_MemoryType response_memory_type;
int64_t response_memory_type_id;
void* userp;
const auto& response_test = responses[idx];
for (const auto& response_test_output : response_test->Outputs()) {
ASSERT_EQ(response_test_output.Name(), response0_output.Name());
ASSERT_EQ(response_test_output.DType(), response0_output.DType());
ASSERT_EQ(response_test_output.Shape(), response0_output.Shape());
helpers::CheckStatus(response_test_output.DataBuffer(
&response_buffer, &response_byte_size, &response_memory_type,
&response_memory_type_id, &userp));
const int* cache_output = static_cast<const int*>(response_buffer);
for (size_t i = 0; i < response_byte_size / sizeof(int); i++) {
ASSERT_EQ(cache_output[i], expected_outputs[i]);
}
}
}
}
void
ParallelLookupInsert(
std::shared_ptr<tc::TritonCache> cache, size_t thread_count,
std::unique_ptr<tc::InferenceResponse>& insert_response,
std::vector<tc::InferenceRequest*>& unique_requests)
{
std::vector<std::thread> insert_threads;
std::vector<std::thread> lookup_threads;
std::vector<std::unique_ptr<tc::InferenceResponse>> responses;
std::cout << "Create responses" << std::endl;
for (size_t idx = 0; idx < thread_count; idx++) {
std::unique_ptr<tc::InferenceResponse> response;
helpers::CheckStatus(
unique_requests[idx]->ResponseFactory()->CreateResponse(&response));
responses.push_back(std::move(response));
}
std::cout << "Insert and Lookup responses into cache with [" << thread_count
<< "] threads in parallel" << std::endl;
for (size_t idx = 0; idx < thread_count; idx++) {
auto key = std::to_string(idx);
insert_threads.emplace_back(std::thread(
&helpers::InsertWrapper, cache, insert_response.get(), key));
lookup_threads.emplace_back(std::thread(
&helpers::LookupWrapperMaybeMiss, cache, responses[idx].get(), key));
}
for (size_t idx = 0; idx < thread_count; idx++) {
insert_threads[idx].join();
lookup_threads[idx].join();
}
}
void
EndToEnd(
std::shared_ptr<tc::TritonCache> cache, tc::InferenceRequest* request,
std::unique_ptr<tc::InferenceResponse>& response,
const std::vector<helpers::Tensor>& expected_outputs)
{
std::string key = "";
helpers::CheckStatus(cache->Hash(*request, &key));
ASSERT_NE(key, "");
std::cout << "Lookup request in empty cache" << std::endl;
auto status = cache->Lookup(nullptr, key);
ASSERT_FALSE(status.IsOk()) << "hash [" + key + "] should not be in cache";
helpers::CheckStatus(cache->Insert(response.get(), key));
status = cache->Insert(response.get(), key);
if (cache->Name() == "redis") {
ASSERT_TRUE(status.IsOk())
<< "Inserting duplicate item in cache should succeed for redis cache";
} else {
ASSERT_FALSE(status.IsOk())
<< "Inserting duplicate item in cache should fail unless "
"implementation "
<< "explicitly allows it and is specified here.";
}
std::cout << "Create response object into fill from cache" << std::endl;
std::unique_ptr<tc::InferenceResponse> response_test;
helpers::CheckStatus(
request->ResponseFactory()->CreateResponse(&response_test));
std::cout << "Lookup request in cache after insertion" << std::endl;
helpers::CheckStatus(cache->Lookup(response_test.get(), key));
const auto& response0_output = response->Outputs()[0];
const void* response_buffer = nullptr;
size_t response_byte_size = 0;
TRITONSERVER_MemoryType response_memory_type;
int64_t response_memory_type_id;
void* userp;
for (const auto& response_test_output : response_test->Outputs()) {
ASSERT_EQ(response_test_output.Name(), response0_output.Name());
ASSERT_EQ(response_test_output.DType(), response0_output.DType());
ASSERT_EQ(response_test_output.Shape(), response0_output.Shape());
helpers::CheckStatus(response_test_output.DataBuffer(
&response_buffer, &response_byte_size, &response_memory_type,
&response_memory_type_id, &userp));
}
const int* cache_output = static_cast<const int*>(response_buffer);
for (size_t i = 0; i < response_byte_size / sizeof(int); i++) {
ASSERT_EQ(cache_output[i], expected_outputs[0].data[i]);
}
}
}
TEST_F(RequestResponseCacheTest, TestLocalCacheSizeTooSmall)
{
constexpr uint64_t cache_size = 1;
auto cache_config = R"({"size": )" + std::to_string(cache_size) + "}";
std::cout << "Create cache of size: " << cache_size << std::endl;
helpers::CreateCacheExpectFail("local", cache_config);
}
TEST_F(RequestResponseCacheTest, TestLocalCacheSizeTooLarge)
{
constexpr uint64_t cache_size = ULLONG_MAX;
auto cache_config = R"({"size": )" + std::to_string(cache_size) + "}";
std::cout << "Create cache of size: " << cache_size << std::endl;
helpers::CreateCacheExpectFail("local", cache_config);
}
TEST_F(RequestResponseCacheTest, TestLocalCacheSizeSmallerThanEntryBytes)
{
constexpr uint64_t cache_size = 4 * 1024 * 1024; auto cache = helpers::CreateLocalCache(cache_size);
ASSERT_NE(cache, nullptr);
std::vector<tc::Byte> large_data(cache_size + 1);
std::vector<boost::span<tc::Byte>> entry;
entry.push_back(large_data);
auto status = cache->Insert(entry, "large_bytes");
ASSERT_FALSE(status.IsOk())
<< "Inserting item larger than cache succeeded when it should fail";
}
TEST_F(RequestResponseCacheTest, TestLocalCacheSizeSmallerThanEntryResponse)
{
constexpr uint64_t cache_size = 4 * 1024 * 1024; auto cache = helpers::CreateLocalCache(cache_size);
ASSERT_NE(cache, nullptr);
std::vector<int> large_data(cache_size + 1, 0);
std::cout << "Create large_response (larger than cache) of size: "
<< large_data.size() << std::endl;
std::vector<helpers::Tensor> large_outputs{
helpers::Tensor{"output", large_data}};
auto large_response = helpers::GenerateResponse(
request0, dtype, memory_type, memory_type_id, large_outputs);
std::cout << "Insert large_response into cache" << std::endl;
auto status = cache->Insert(large_response.get(), "large_response");
ASSERT_FALSE(status.IsOk())
<< "Inserting item larger than cache succeeded when it should fail";
}
TEST_F(RequestResponseCacheTest, TestLocalCacheEvictionLRU)
{
auto cache = helpers::CreateLocalCache(1200);
ASSERT_NE(cache, nullptr);
helpers::CheckStatus(cache->Insert(response_400bytes.get(), "request0"));
helpers::CheckStatus(cache->Insert(response_400bytes.get(), "request1"));
tc::CacheEntry entry0, entry1, entry2, entry3, entry4, entry5, entry6, entry7;
auto status = cache->Lookup("request0", &entry0);
ASSERT_TRUE(status.IsOk()) << status.Message();
ASSERT_TRUE(cache->Lookup("request1", &entry1).IsOk());
helpers::CheckStatus(cache->Insert(response_400bytes.get(), "request2"));
ASSERT_TRUE(cache->Lookup("request2", &entry2).IsOk());
ASSERT_FALSE(cache->Lookup("request0", &entry3).IsOk());
ASSERT_TRUE(cache->Lookup("request1", &entry4).IsOk());
helpers::CheckStatus(cache->Insert(response_400bytes.get(), "request3"));
ASSERT_TRUE(cache->Lookup("request3", &entry5).IsOk());
ASSERT_TRUE(cache->Lookup("request1", &entry6).IsOk());
ASSERT_FALSE(cache->Lookup("request2", &entry7).IsOk());
}
TEST_F(RequestResponseCacheTest, TestLocalCacheInsertLookupCompareBytes)
{
auto cache = helpers::CreateLocalCache(1024);
ASSERT_NE(cache, nullptr);
tests::InsertLookupCompareBytes(cache);
}
TEST_F(RequestResponseCacheTest, TestLocalCacheHashing)
{
auto cache = helpers::CreateLocalCache(1024);
ASSERT_NE(cache, nullptr);
tests::HashLogic(cache, request0, request1, request2, request3, request4);
tests::HashUnique(cache, unique_requests);
}
TEST_F(RequestResponseCacheTest, TestLocalCacheParallelInsert)
{
auto cache = helpers::CreateLocalCache(1200);
ASSERT_NE(cache, nullptr);
const size_t expected_cache_hits = 2;
tests::ParallelInsert(
cache, thread_count, response_400bytes, expected_cache_hits);
}
TEST_F(RequestResponseCacheTest, TestLocalCacheParallelLookup)
{
auto cache = helpers::CreateLocalCache(2 * thread_count * output100_size);
ASSERT_NE(cache, nullptr);
tests::ParallelLookup(
cache, thread_count, response_400bytes, unique_requests, data100);
}
TEST_F(RequestResponseCacheTest, TestLocalCacheParallelLookupInsert)
{
auto cache = helpers::CreateLocalCache(1024);
ASSERT_NE(cache, nullptr);
tests::ParallelLookupInsert(
cache, thread_count, response_400bytes, unique_requests);
}
TEST_F(RequestResponseCacheTest, TestLocalCacheEndToEnd)
{
auto cache = helpers::CreateLocalCache(8 * 1024 * 1024);
ASSERT_NE(cache, nullptr);
tests::EndToEnd(cache, request0, response0, outputs0);
}
TEST_F(RequestResponseCacheTest, TestRedisCacheInsertLookupCompareBytes)
{
auto cache = helpers::CreateRedisCache(redis_host, redis_port);
ASSERT_NE(cache, nullptr);
tests::InsertLookupCompareBytes(cache);
}
TEST_F(RequestResponseCacheTest, TestRedisCacheHashing)
{
auto cache = helpers::CreateRedisCache(redis_host, redis_port);
ASSERT_NE(cache, nullptr);
tests::HashLogic(cache, request0, request1, request2, request3, request4);
tests::HashUnique(cache, unique_requests);
}
TEST_F(RequestResponseCacheTest, TestRedisCacheParallelInsert)
{
auto cache = helpers::CreateRedisCache(redis_host, redis_port);
ASSERT_NE(cache, nullptr);
const size_t expected_cache_hits = thread_count;
tests::ParallelInsert(
cache, thread_count, response_400bytes, expected_cache_hits);
}
TEST_F(RequestResponseCacheTest, TestRedisCacheParallelLookup)
{
auto cache = helpers::CreateRedisCache(redis_host, redis_port);
ASSERT_NE(cache, nullptr);
tests::ParallelLookup(
cache, thread_count, response_400bytes, unique_requests, data100);
}
TEST_F(RequestResponseCacheTest, TestRedisCacheParallelLookupInsert)
{
auto cache = helpers::CreateRedisCache(redis_host, redis_port);
ASSERT_NE(cache, nullptr);
tests::ParallelLookupInsert(
cache, thread_count, response_400bytes, unique_requests);
}
TEST_F(RequestResponseCacheTest, TestRedisCacheEndToEnd)
{
auto cache = helpers::CreateRedisCache(redis_host, redis_port);
ASSERT_NE(cache, nullptr);
tests::EndToEnd(cache, request0, response0, outputs0);
}
}
int
main(int argc, char** argv)
{
#ifdef TRITON_ENABLE_LOGGING
LOG_SET_VERBOSE(2);
#endif
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}