Files
wehub-resource-sync 770d92cb1f
Lint / lint (push) Waiting to run
Windows CI / Windows (push) Waiting to run
Build Docs / Deploy Docs (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

418 lines
16 KiB
C++

/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/threaded_engine.cc
* \brief The implementation for threaded serving engine in MLC LLM.
*/
#include "threaded_engine.h"
#include <tvm/ffi/extra/module.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <optional>
#include "../support/json_parser.h"
#include "../support/module_vtable.h"
#include "../support/result.h"
#include "engine.h"
#include "request.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::Device;
using namespace tvm::runtime;
/*! \brief The threaded engine instruction kind. */
enum class InstructionKind : int {
kAddRequest = 0,
kAbortRequest = 1,
kUnloadEngine = 2,
kReloadEngine = 3,
kResetEngine = 4,
kDebugCallFuncOnAllAllWorker = 5,
};
/*! \brief The implementation of ThreadedEngine. */
class ThreadedEngineImpl : public ThreadedEngine {
public:
void InitThreadedEngine(Device device, Optional<Function> request_stream_callback,
Optional<EventTraceRecorder> trace_recorder) final {
device_ = device;
TVM_FFI_ICHECK(request_stream_callback.has_value())
<< "ThreadedEngine requires request stream callback function, but it is not given.";
request_stream_callback_ = request_stream_callback.value();
trace_recorder_ = trace_recorder;
}
void Reload(String engine_config_json_str) final {
// NOTE: important to set this before, we send out
// reload instruction to the other threads
// otherwise there can be deadlocks
reload_finished_ = false;
bool need_notify = false;
{
std::lock_guard<std::mutex> lock(background_loop_mutex_);
instruction_queue_.emplace_back(InstructionKind::kReloadEngine,
std::move(engine_config_json_str));
++pending_request_operation_cnt_;
need_notify = engine_waiting_;
}
if (need_notify) {
background_loop_cv_.notify_one();
}
{
std::unique_lock<std::mutex> lock(reload_unload_mutex_);
reload_unload_cv_.wait(lock, [this] { return reload_finished_; });
}
}
void Unload() final {
// NOTE: important to set this before, we send out
// reload instruction to the other threads
// otherwise there can be deadlocks
// e.g. the other thread finish unload job and set the flag to true
// then we set it back to false
unload_finished_ = false;
bool need_notify = false;
{
std::lock_guard<std::mutex> lock(background_loop_mutex_);
instruction_queue_.emplace_back(InstructionKind::kUnloadEngine, ObjectRef(nullptr));
++pending_request_operation_cnt_;
need_notify = engine_waiting_;
}
if (need_notify) {
background_loop_cv_.notify_one();
}
{
std::unique_lock<std::mutex> lock(reload_unload_mutex_);
reload_unload_cv_.wait(lock, [this] { return unload_finished_; });
}
}
void Reset() final {
bool need_notify = false;
{
std::lock_guard<std::mutex> lock(background_loop_mutex_);
instruction_queue_.emplace_back(InstructionKind::kResetEngine, ObjectRef(nullptr));
++pending_request_operation_cnt_;
need_notify = engine_waiting_;
}
if (need_notify) {
background_loop_cv_.notify_one();
}
}
void AddRequest(Request request) final {
bool need_notify = false;
{
std::lock_guard<std::mutex> lock(background_loop_mutex_);
instruction_queue_.emplace_back(InstructionKind::kAddRequest, request);
++pending_request_operation_cnt_;
need_notify = engine_waiting_;
}
if (need_notify) {
background_loop_cv_.notify_one();
}
}
void AbortRequest(const String& request_id) final {
bool need_notify = false;
{
std::lock_guard<std::mutex> lock(background_loop_mutex_);
instruction_queue_.emplace_back(InstructionKind::kAbortRequest, request_id);
++pending_request_operation_cnt_;
need_notify = engine_waiting_;
}
if (need_notify) {
background_loop_cv_.notify_one();
}
}
void RunBackgroundLoop() final {
// The local vectors that load the requests from critical regions.
std::vector<std::pair<InstructionKind, Any>> local_instruction_queue;
while (!exit_now_.load(std::memory_order_relaxed)) {
{
std::unique_lock<std::mutex> lock(background_loop_mutex_);
engine_waiting_ = true;
background_loop_cv_.wait(lock, [this] {
return (background_engine_ != nullptr && !background_engine_->Empty()) ||
pending_request_operation_cnt_.load() > 0 ||
exit_now_.load(std::memory_order_relaxed);
});
engine_waiting_ = false;
local_instruction_queue = instruction_queue_;
instruction_queue_.clear();
pending_request_operation_cnt_ = 0;
}
for (const auto& [kind, arg] : local_instruction_queue) {
if (kind == InstructionKind::kAddRequest) {
TVM_FFI_ICHECK(background_engine_ != nullptr) << "Background engine is not loaded.";
background_engine_->AddRequest(arg.as_or_throw<Request>());
} else if (kind == InstructionKind::kAbortRequest) {
// in a rare case, abort request can happen after unloading
// aka background engine is nullptr
// this happens when the on going generation was interrupted
// the engine get unloaded, and then abort was called.
// it is safe to ignore these abort in such case
if (background_engine_ != nullptr) {
background_engine_->AbortRequest(arg.as_or_throw<String>());
}
} else if (kind == InstructionKind::kUnloadEngine) {
EngineUnloadImpl();
} else if (kind == InstructionKind::kReloadEngine) {
EngineUnloadImpl();
EngineReloadImpl(arg.as_or_throw<String>());
} else if (kind == InstructionKind::kResetEngine) {
if (background_engine_ != nullptr) {
background_engine_->Reset();
}
} else if (kind == InstructionKind::kDebugCallFuncOnAllAllWorker) {
TVM_FFI_ICHECK(background_engine_ != nullptr) << "Background engine is not loaded.";
Array<Any> packed_args = arg.as_or_throw<Array<Any>>();
background_engine_->DebugCallFuncOnAllAllWorker(
packed_args[0].as_or_throw<String>(), packed_args[1].as_or_throw<Optional<String>>());
} else {
LOG(FATAL) << "Cannot reach here";
}
}
if (background_engine_ != nullptr) {
background_engine_->Step();
}
}
}
void RunBackgroundStreamBackLoop() final {
// The local vectors that load the request stream callback inputs from critical regions.
std::vector<Array<RequestStreamOutput>> local_request_stream_callback_inputs;
std::vector<RequestStreamOutput> flattened_callback_inputs;
while (!exit_now_.load(std::memory_order_relaxed)) {
{
std::unique_lock<std::mutex> lock(request_stream_callback_mutex_);
stream_callback_waiting_ = true;
request_stream_callback_cv_.wait(lock, [this] {
return pending_request_stream_callback_cnt_.load() > 0 ||
exit_now_.load(std::memory_order_relaxed);
});
stream_callback_waiting_ = false;
local_request_stream_callback_inputs = request_stream_callback_inputs_;
request_stream_callback_inputs_.clear();
pending_request_stream_callback_cnt_ = 0;
}
for (const Array<RequestStreamOutput>& callback_inputs :
local_request_stream_callback_inputs) {
for (const RequestStreamOutput& callback_input : callback_inputs) {
flattened_callback_inputs.push_back(callback_input);
}
}
if (!flattened_callback_inputs.empty()) {
request_stream_callback_(Array<RequestStreamOutput>(flattened_callback_inputs));
}
flattened_callback_inputs.clear();
}
}
void ExitBackgroundLoop() final {
{
std::lock_guard<std::mutex> lock(background_loop_mutex_);
exit_now_.store(true);
}
background_loop_cv_.notify_one();
request_stream_callback_cv_.notify_one();
}
/************** Query/Profile/Debug **************/
GenerationConfig GetDefaultGenerationConfig() const final {
TVM_FFI_ICHECK(default_generation_config_.has_value())
<< "The default generation config has not been set.";
return default_generation_config_.value();
}
Request CreateRequest(String id, Array<Data> inputs, String generation_cfg_json_str) const {
json::Object config = json::ParseToJSONObject(generation_cfg_json_str);
auto gen_config = GenerationConfig::FromJSON(config, GetDefaultGenerationConfig());
TVM_FFI_ICHECK(gen_config.IsOk()) << gen_config.UnwrapErr();
return Request(std::move(id), std::move(inputs), gen_config.Unwrap());
}
EngineConfig GetCompleteEngineConfig() const final {
TVM_FFI_ICHECK(complete_engine_config_.has_value()) << "The engine config has not been set.";
return complete_engine_config_.value();
}
String GetCompleteEngineConfigJSONString() const {
return GetCompleteEngineConfig()->AsJSONString();
}
void DebugCallFuncOnAllAllWorker(const String& func_name, Optional<String> func_args) final {
bool need_notify = false;
{
std::lock_guard<std::mutex> lock(background_loop_mutex_);
instruction_queue_.emplace_back(InstructionKind::kDebugCallFuncOnAllAllWorker,
Array<Any>{func_name, func_args});
++pending_request_operation_cnt_;
need_notify = engine_waiting_;
}
if (need_notify) {
background_loop_cv_.notify_one();
}
}
private:
void EngineReloadImpl(const std::string& engine_config_json_str) {
auto frequest_stream_callback_wrapper = [this](Array<RequestStreamOutput> delta_outputs) {
bool need_notify = false;
{
std::lock_guard<std::mutex> lock(request_stream_callback_mutex_);
request_stream_callback_inputs_.push_back(std::move(delta_outputs));
++pending_request_stream_callback_cnt_;
need_notify = stream_callback_waiting_;
}
if (need_notify) {
request_stream_callback_cv_.notify_one();
}
};
FRequestStreamCallback request_stream_callback(frequest_stream_callback_wrapper);
Result<EngineCreationOutput> output_res =
Engine::Create(engine_config_json_str, device_, request_stream_callback, trace_recorder_);
TVM_FFI_ICHECK(output_res.IsOk()) << output_res.UnwrapErr();
EngineCreationOutput output = output_res.Unwrap();
background_engine_ = std::move(output.reloaded_engine);
default_generation_config_ = output.default_generation_cfg;
complete_engine_config_ = output.completed_engine_config;
{
// Wake up the thread waiting for reload finish.
std::lock_guard<std::mutex> lock(reload_unload_mutex_);
reload_finished_ = true;
}
reload_unload_cv_.notify_one();
}
void EngineUnloadImpl() {
if (background_engine_ != nullptr) {
background_engine_->AbortAllRequests();
background_engine_ = nullptr;
// Clear the allocated memory in cached memory pool.
static Function fclear_memory_manager =
Function::GetGlobalRequired("vm.builtin.memory_manager.clear");
fclear_memory_manager();
default_generation_config_ = std::nullopt;
complete_engine_config_ = std::nullopt;
}
{
// Wake up the thread waiting for unload finish.
std::lock_guard<std::mutex> lock(reload_unload_mutex_);
unload_finished_ = true;
}
reload_unload_cv_.notify_one();
}
/*! \brief The device to run models on. */
Device device_;
/*! \brief The background normal engine for request processing. */
std::unique_ptr<Engine> background_engine_;
/*! \brief The request stream callback. */
Function request_stream_callback_;
/*! \brief Event trace recorder. */
Optional<EventTraceRecorder> trace_recorder_;
/*! \brief complete engine config. */
Optional<EngineConfig> complete_engine_config_;
/*! \brief The default generation config. */
Optional<GenerationConfig> default_generation_config_;
/*! \brief The mutex ensuring only one thread can access critical regions. */
std::mutex background_loop_mutex_;
std::mutex request_stream_callback_mutex_;
std::mutex reload_unload_mutex_;
/*! \brief The condition variable preventing threaded engine from spinning. */
std::condition_variable background_loop_cv_;
std::condition_variable request_stream_callback_cv_;
std::condition_variable reload_unload_cv_;
/*! \brief A boolean flag denoting if the engine needs to exit background loop. */
std::atomic<bool> exit_now_ = false;
/************** Critical Regions **************/
/*!
* \brief The instruction queue for the threaded engine.
* The instructions include:
* - requests to add into the background engine,
* - requests to abort from the background engine,
* - engine unload/reload,
* - and other debugging instructions.
* Elements are sended from other threads and consumed by
* the threaded engine in the background loop.
*/
std::vector<std::pair<InstructionKind, Any>> instruction_queue_;
/*!
* \brief The delta outputs to pass through callback.
* Elements are sended from the background loop thread and
* consumed by the foreground thread.
*/
std::vector<Array<RequestStreamOutput>> request_stream_callback_inputs_;
/*!
* \brief Number of pending request operations, should be the size of
* `requests_to_add_` and `requests_to_abort_`.
*/
std::atomic<int> pending_request_operation_cnt_ = 0;
/*!
* \brief Number of pending request stream callback invocations.
* It should be the size of `request_stream_callback_inputs_`.
*/
std::atomic<int> pending_request_stream_callback_cnt_ = 0;
/*! \brief A boolean flag indicating if the engine is waiting for new requests/aborts. */
bool engine_waiting_ = false;
/*! \brief A boolean flag indicating if the stream callback loop is waiting. */
bool stream_callback_waiting_ = false;
/*! \brief A boolean indicating if the engine reload has finished. */
bool reload_finished_ = false;
/*! \brief A boolean indicating if the engine unload has finished. */
bool unload_finished_ = false;
};
/*! \brief The implementation of ThreadedEngine. */
class ThreadedEngineModule : public ThreadedEngineImpl, public ffi::ModuleObj {
public:
TVM_MODULE_VTABLE_BEGIN("mlc.serve.async_threaded_engine");
TVM_MODULE_VTABLE_ENTRY("init_threaded_engine", &ThreadedEngineImpl::InitThreadedEngine);
TVM_MODULE_VTABLE_ENTRY("reload", &ThreadedEngineImpl::Reload);
TVM_MODULE_VTABLE_ENTRY("add_request", &ThreadedEngineImpl::AddRequest);
TVM_MODULE_VTABLE_ENTRY("create_request", &ThreadedEngineImpl::CreateRequest);
TVM_MODULE_VTABLE_ENTRY("abort_request", &ThreadedEngineImpl::AbortRequest);
TVM_MODULE_VTABLE_ENTRY("run_background_loop", &ThreadedEngineImpl::RunBackgroundLoop);
TVM_MODULE_VTABLE_ENTRY("run_background_stream_back_loop",
&ThreadedEngineImpl::RunBackgroundStreamBackLoop);
TVM_MODULE_VTABLE_ENTRY("exit_background_loop", &ThreadedEngineImpl::ExitBackgroundLoop);
TVM_MODULE_VTABLE_ENTRY("get_complete_engine_config",
&ThreadedEngineImpl::GetCompleteEngineConfigJSONString);
TVM_MODULE_VTABLE_ENTRY("reset", &ThreadedEngineImpl::Reset);
TVM_MODULE_VTABLE_ENTRY("debug_call_func_on_all_worker",
&ThreadedEngineImpl::DebugCallFuncOnAllAllWorker);
TVM_MODULE_VTABLE_END();
};
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("mlc.serve.create_threaded_engine",
[]() { return Module(tvm::ffi::make_object<ThreadedEngineModule>()); });
}
std::unique_ptr<ThreadedEngine> ThreadedEngine::Create() {
std::unique_ptr<ThreadedEngineImpl> threaded_engine = std::make_unique<ThreadedEngineImpl>();
return std::move(threaded_engine);
}
} // namespace serve
} // namespace llm
} // namespace mlc