#pragma once
#include <chrono>
#include <future>
#include "batch_reader.h"
#include "models/model.h"
#include "thread_pool.h"
#include "utils.h"
namespace ctranslate2 {
struct ReplicaPoolConfig {
size_t num_threads_per_replica = 0;
long max_queued_batches = 0;
int cpu_core_offset = -1;
};
template <typename Replica>
class ReplicaWorker;
template <typename Replica>
class ReplicaPool {
public:
virtual ~ReplicaPool() = default;
ReplicaPool(const models::ModelLoader& model_loader,
const ReplicaPoolConfig& config = {}) {
initialize_pool(model_loader, config);
}
ReplicaPool(const std::string& model_path,
const Device device,
const ComputeType compute_type = ComputeType::DEFAULT,
const std::vector<int>& device_indices = {0},
const bool tensor_parallel = false,
const ReplicaPoolConfig& config = {}) {
models::ModelLoader model_loader(model_path);
model_loader.device = device;
model_loader.device_indices = device_indices;
model_loader.compute_type = compute_type;
model_loader.tensor_parallel = tensor_parallel;
initialize_pool(model_loader, config);
}
ReplicaPool(const std::shared_ptr<const models::Model>& model,
const ReplicaPoolConfig& config = {}) {
initialize_pool({model}, config);
}
ReplicaPool(const std::vector<std::shared_ptr<const models::Model>>& replicas,
const ReplicaPoolConfig& config = {}) {
initialize_pool(replicas, config);
}
template <typename Result, typename Func>
std::future<Result> post(Func func) {
auto batched_func = [func = std::move(func)](Replica& replica) mutable {
std::vector<Result> results;
results.reserve(1);
results.emplace_back(func(replica));
return results;
};
auto futures = post_batch<Result>(std::move(batched_func), 1);
return std::move(futures[0]);
}
template <typename Result, typename Func>
std::vector<std::future<Result>> post_batch(Func func, size_t num_results) {
std::vector<std::promise<Result>> promises(num_results);
std::vector<std::future<Result>> futures;
futures.reserve(promises.size());
for (auto& promise : promises)
futures.emplace_back(promise.get_future());
post_batch(std::move(func), std::move(promises));
return futures;
}
template <typename Result, typename Func>
void post_batch(Func func, std::vector<std::promise<Result>> promises) {
auto wrapped_func = [func = std::move(func)]() mutable {
return func(get_thread_replica());
};
post_func(std::move(wrapped_func), std::move(promises));
}
size_t num_queued_batches() const {
return _thread_pool->num_queued_jobs();
}
size_t num_active_batches() const {
return _thread_pool->num_active_jobs();
}
size_t num_replicas() const {
return _thread_pool->num_threads();
}
std::vector<std::shared_ptr<const models::Model>> detach_models() {
std::vector<std::shared_ptr<const models::Model>> models;
models.reserve(num_replicas());
for (size_t i = 0; i < num_replicas(); ++i) {
auto& worker = static_cast<ReplicaWorker<Replica>&>(_thread_pool->get_worker(i));
models.emplace_back(worker.detach_model());
}
return models;
}
void set_models(const std::vector<std::shared_ptr<const models::Model>>& models) {
if (models.size() != num_replicas())
throw std::invalid_argument("The number of models does not match the number "
"of parallel replicas");
for (size_t i = 0; i < num_replicas(); ++i) {
auto& worker = static_cast<ReplicaWorker<Replica>&>(_thread_pool->get_worker(i));
worker.set_model(models[i]);
}
}
void clear_cache() const {
for (size_t i = 0; i < num_replicas(); ++i) {
auto& worker = static_cast<ReplicaWorker<Replica>&>(_thread_pool->get_worker(i));
auto* allocator = worker.allocator();
if (allocator)
allocator->clear_cache();
}
}
const Replica& get_first_replica() const {
auto& worker = static_cast<ReplicaWorker<Replica>&>(_thread_pool->get_worker(0));
return worker.replica();
}
protected:
template <typename Result, typename Func>
std::vector<std::future<Result>>
post_examples(const std::vector<Example>& examples,
size_t max_batch_size,
BatchType batch_type,
const Func& func) {
std::vector<std::promise<Result>> promises(examples.size());
std::vector<std::future<Result>> futures;
futures.reserve(promises.size());
for (auto& promise : promises)
futures.emplace_back(promise.get_future());
post_examples(examples, max_batch_size, batch_type, std::move(promises), func);
return futures;
}
template <typename Result, typename Func>
void post_examples(const std::vector<Example>& examples,
size_t max_batch_size,
BatchType batch_type,
std::vector<std::promise<Result>> promises,
const Func& func) {
for (auto& batch : rebatch_input(examples, max_batch_size, batch_type)) {
std::vector<std::promise<Result>> batch_promises;
batch_promises.reserve(batch.num_examples());
for (const size_t index : batch.example_index)
batch_promises.emplace_back(std::move(promises[index]));
post_batch<Result>(
[batch = std::move(batch), func](Replica& replica) { return func(replica, batch); },
std::move(batch_promises));
}
}
template <typename Result, typename ResultWriter, typename Func>
void consume_batches(BatchReader& batch_reader,
ResultWriter& result_writer,
const Func& func,
size_t max_batch_size,
size_t read_batch_size,
BatchType batch_type) {
std::queue<std::future<Result>> results;
auto pop_results = [&results, &result_writer](bool blocking) {
constexpr std::chrono::seconds zero_sec(0);
while (!results.empty()
&& (blocking
|| results.front().wait_for(zero_sec) == std::future_status::ready)) {
result_writer(results.front().get());
results.pop();
}
};
if (read_batch_size == 0)
read_batch_size = (max_batch_size == 1 ? max_batch_size : max_batch_size * 16);
while (true) {
auto examples = batch_reader.get_next(read_batch_size, batch_type);
if (examples.empty())
break;
auto futures = post_examples<Result>(examples, max_batch_size, batch_type, func);
for (auto& future : futures)
results.emplace(std::move(future));
pop_results(false);
}
pop_results(true);
}
private:
std::unique_ptr<ThreadPool> _thread_pool;
static Replica& get_thread_replica() {
auto& worker = static_cast<ReplicaWorker<Replica>&>(ThreadPool::get_local_worker());
return worker.replica();
}
void initialize_pool(const models::ModelLoader& model_loader,
const ReplicaPoolConfig& config) {
set_num_threads(config.num_threads_per_replica);
initialize_pool(model_loader.load(), config);
}
void initialize_pool(const std::vector<std::shared_ptr<const models::Model>>& models,
const ReplicaPoolConfig& config) {
std::vector<std::unique_ptr<Worker>> workers;
workers.reserve(models.size());
for (const auto& model : models) {
workers.emplace_back(std::make_unique<ReplicaWorker<Replica>>(model, config.num_threads_per_replica));
}
size_t max_queue_size = std::numeric_limits<size_t>::max();
if (config.max_queued_batches == 0)
max_queue_size = 4 * workers.size();
else if (config.max_queued_batches > 0)
max_queue_size = config.max_queued_batches;
_thread_pool = std::make_unique<ThreadPool>(std::move(workers),
max_queue_size,
config.cpu_core_offset);
}
template <typename Result, typename Func>
void post_func(Func func, std::vector<std::promise<Result>> promises) {
_thread_pool->post(std::make_unique<BatchJob<Result, Func>>(std::move(promises),
std::move(func)));
}
template <typename Result, typename Func>
class BatchJob : public Job {
public:
BatchJob(std::vector<std::promise<Result>> promises, Func func)
: _promises(std::move(promises))
, _func(std::move(func))
{
}
void run() override {
std::vector<Result> results;
std::exception_ptr exception;
try {
results = _func();
} catch (...) {
exception = std::current_exception();
}
for (size_t i = 0; i < _promises.size(); ++i) {
if (exception)
_promises[i].set_exception(exception);
else
_promises[i].set_value(std::move(results[i]));
}
}
private:
std::vector<std::promise<Result>> _promises;
Func _func;
};
};
template <typename Replica>
class ReplicaWorker : public Worker {
public:
ReplicaWorker(const std::shared_ptr<const models::Model>& model, size_t num_threads)
: _device(model->device())
, _device_index(model->device_index())
, _num_threads(num_threads)
, _allocator(nullptr)
{
set_model(model);
}
Replica& replica() {
if (!_replica)
throw std::runtime_error("No model replica is available in this thread");
return *_replica;
}
void set_model(const std::shared_ptr<const models::Model>& model) {
_replica = Replica::create_from_model(*model);
}
std::shared_ptr<const models::Model> detach_model() {
if (!_replica)
return nullptr;
auto model = _replica->model();
_replica.reset();
return model;
}
Allocator* allocator() {
return _allocator;
}
protected:
void initialize() override {
set_device_index(_device, _device_index);
set_num_threads(_num_threads);
_allocator = &get_allocator(_device);
}
void idle() override {
synchronize_stream(_device);
}
void finalize() override {
_replica.reset();
destroy_context(_device);
}
private:
const Device _device;
const int _device_index;
const size_t _num_threads;
Allocator* _allocator;
std::unique_ptr<Replica> _replica;
};
}