/*! * 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 #include #include #include #include #include #include #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 request_stream_callback, Optional 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 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 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 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 lock(reload_unload_mutex_); reload_unload_cv_.wait(lock, [this] { return unload_finished_; }); } } void Reset() final { bool need_notify = false; { std::lock_guard 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 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 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> local_instruction_queue; while (!exit_now_.load(std::memory_order_relaxed)) { { std::unique_lock 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()); } 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()); } } else if (kind == InstructionKind::kUnloadEngine) { EngineUnloadImpl(); } else if (kind == InstructionKind::kReloadEngine) { EngineUnloadImpl(); EngineReloadImpl(arg.as_or_throw()); } 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 packed_args = arg.as_or_throw>(); background_engine_->DebugCallFuncOnAllAllWorker( packed_args[0].as_or_throw(), packed_args[1].as_or_throw>()); } 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> local_request_stream_callback_inputs; std::vector flattened_callback_inputs; while (!exit_now_.load(std::memory_order_relaxed)) { { std::unique_lock 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& 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(flattened_callback_inputs)); } flattened_callback_inputs.clear(); } } void ExitBackgroundLoop() final { { std::lock_guard 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 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 func_args) final { bool need_notify = false; { std::lock_guard lock(background_loop_mutex_); instruction_queue_.emplace_back(InstructionKind::kDebugCallFuncOnAllAllWorker, Array{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 delta_outputs) { bool need_notify = false; { std::lock_guard 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 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 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 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 background_engine_; /*! \brief The request stream callback. */ Function request_stream_callback_; /*! \brief Event trace recorder. */ Optional trace_recorder_; /*! \brief complete engine config. */ Optional complete_engine_config_; /*! \brief The default generation config. */ Optional 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 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> 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> request_stream_callback_inputs_; /*! * \brief Number of pending request operations, should be the size of * `requests_to_add_` and `requests_to_abort_`. */ std::atomic 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 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()); }); } std::unique_ptr ThreadedEngine::Create() { std::unique_ptr threaded_engine = std::make_unique(); return std::move(threaded_engine); } } // namespace serve } // namespace llm } // namespace mlc