/*! * Copyright (c) 2023-2025 by Contributors * \file serve/config.cc */ #include "config.h" #include #include #include #include #include "../json_ffi/openai_api_protocol.h" #include "../support/json_parser.h" #include "../support/utils.h" #include "data.h" namespace mlc { namespace llm { namespace serve { TVM_FFI_STATIC_INIT_BLOCK() { GenerationConfigNode::RegisterReflection(); EngineConfigNode::RegisterReflection(); } uint64_t TotalDetectGlobalMemory(DLDevice device) { // Get single-card GPU size. tvm::ffi::Any rv; DeviceAPI::Get(device)->GetAttr(device, DeviceAttrKind::kTotalGlobalMemory, &rv); int64_t gpu_size_bytes = rv.cast(); // Since the memory size returned by the OpenCL runtime is smaller than the actual available // memory space, we set a best available space so that MLC LLM can run 7B or 8B models on Android // with OpenCL. if (device.device_type == kDLOpenCL) { int64_t min_size_bytes = 5LL * 1024 * 1024 * 1024; // Minimum size is 5 GB gpu_size_bytes = std::max(gpu_size_bytes, min_size_bytes); } return gpu_size_bytes; } /****************** ResponseFormat ******************/ Result ResponseFormat::FromJSON(const tvm::ffi::json::Object& config) { using TResult = Result; ResponseFormat res; res.type = json::LookupOrDefault(config, "type", "text"); std::optional schema = json::LookupOptional(config, "schema"); if (schema.has_value()) { res.schema = schema.value(); } if (res.type != "text" && res.type != "function" && res.type != "json_object") { return TResult::Error("Uknonwn response_format type " + res.type); } return TResult::Ok(res); } tvm::ffi::json::Object ResponseFormat::AsJSON() const { tvm::ffi::json::Object config; config.Set("type", type); if (schema.has_value()) { config.Set("schema", schema.value()); } return config; } /****************** DisaggConfig ******************/ Result DisaggConfig::FromJSON(const tvm::ffi::json::Object& config) { using TResult = Result; DisaggConfig res; std::optional kind = json::LookupOptional(config, "kind"); if (kind.has_value()) { if (kind.value() == "prepare_receive") { res.kind = DisaggRequestKind::kPrepareReceive; } else if (kind.value() == "remote_send") { res.kind = DisaggRequestKind::kRemoteSend; } else if (kind.value() == "start_generation") { res.kind = DisaggRequestKind::kStartGeneration; } else { return TResult::Error("Unknown disaggregation request kind " + kind.value()); } } std::optional kv_append_metadata_encoded = json::LookupOptional(config, "kv_append_metadata"); if (kv_append_metadata_encoded.has_value()) { tvm::ffi::String err; auto parse_result = tvm::ffi::json::Parse(Base64Decode(kv_append_metadata_encoded.value()), &err); if (!err.empty()) { return TResult::Error("kv_append_metadata parse error: " + std::string(err)); } if (!parse_result.try_cast().has_value()) { return TResult::Error("kv_append_metadata is not array of integer."); } tvm::ffi::json::Array kv_append_metadata_arr = parse_result.cast(); std::vector kv_append_metadata; int ptr = 0; while (ptr < static_cast(kv_append_metadata_arr.size())) { if (!kv_append_metadata_arr[ptr].try_cast().has_value()) { return TResult::Error("Invalid kv append metadata value in kv_append_metadata array"); } int num_segments = kv_append_metadata_arr[ptr].cast(); if (ptr + num_segments * 2 + 1 > static_cast(kv_append_metadata_arr.size())) { return TResult::Error("Invalid kv append metadata compression in kv_append_metadata"); } std::vector compressed_kv_append_metadata{num_segments}; compressed_kv_append_metadata.reserve(num_segments * 2 + 1); for (int i = 1; i <= num_segments * 2; ++i) { if (!kv_append_metadata_arr[ptr + i].try_cast().has_value()) { return TResult::Error("Invalid kv append metadata value in kv_append_metadata array"); } compressed_kv_append_metadata.push_back(kv_append_metadata_arr[ptr + i].cast()); } kv_append_metadata.push_back(Shape(std::move(compressed_kv_append_metadata))); ptr += num_segments * 2 + 1; } res.kv_append_metadata = std::move(kv_append_metadata); } res.kv_window_begin = json::LookupOptional(config, "kv_window_begin"); res.kv_window_end = json::LookupOptional(config, "kv_window_end"); res.dst_group_offset = json::LookupOptional(config, "dst_group_offset"); return TResult::Ok(res); } tvm::ffi::json::Object DisaggConfig::AsJSON() const { tvm::ffi::json::Object config; switch (kind) { case DisaggRequestKind::kPrepareReceive: { config.Set("kind", "prepare_receive"); break; } case DisaggRequestKind::kRemoteSend: { config.Set("kind", "remote_send"); break; } case DisaggRequestKind::kStartGeneration: { config.Set("kind", "start_generation"); break; } default: break; } if (!kv_append_metadata.empty()) { tvm::ffi::json::Array kv_append_metadata_arr; for (const Shape& compressed_kv_append_metadata : kv_append_metadata) { for (int64_t value : compressed_kv_append_metadata) { kv_append_metadata_arr.push_back(value); } } config.Set("kv_append_metadata", Base64Encode(tvm::ffi::json::Stringify(kv_append_metadata_arr))); } if (kv_window_begin.has_value()) { config.Set("kv_window_begin", static_cast(kv_window_begin.value())); } if (kv_window_end.has_value()) { config.Set("kv_window_end", static_cast(kv_window_end.value())); } if (dst_group_offset.has_value()) { config.Set("dst_group_offset", static_cast(dst_group_offset.value())); } return config; } /****************** DebugConfig ******************/ Result DebugConfig::FromJSON(const tvm::ffi::json::Object& config) { using TResult = Result; DebugConfig res; res.ignore_eos = json::LookupOrDefault(config, "ignore_eos", false); res.pinned_system_prompt = json::LookupOrDefault(config, "pinned_system_prompt", false); std::string special_request = json::LookupOrDefault(config, "special_request", ""); if (special_request.length() != 0) { if (special_request == "query_engine_metrics") { res.special_request = SpecialRequestKind::kQueryEngineMetrics; } else { return TResult::Error("Unknown special request " + special_request); } } std::string grammar_execution_mode = json::LookupOrDefault(config, "grammar_execution_mode", "jump_forward"); if (grammar_execution_mode == "jump_forward") { res.grammar_execution_mode = GrammarExecutionMode::kJumpForward; } else if (grammar_execution_mode == "constraint") { res.grammar_execution_mode = GrammarExecutionMode::kConstraint; } else { return TResult::Error("Unknown grammar execution mode " + grammar_execution_mode); } if (auto disagg_config_obj = json::LookupOptional(config, "disagg_config")) { Result disagg_config = DisaggConfig::FromJSON(disagg_config_obj.value()); if (disagg_config.IsErr()) { return TResult::Error(disagg_config.UnwrapErr()); } res.disagg_config = disagg_config.Unwrap(); } return TResult::Ok(res); } /** * \return serialized json value of the config. */ tvm::ffi::json::Object DebugConfig::AsJSON() const { tvm::ffi::json::Object config; config.Set("ignore_eos", ignore_eos); config.Set("pinned_system_prompt", pinned_system_prompt); switch (special_request) { case SpecialRequestKind::kQueryEngineMetrics: { config.Set("special_request", "query_engine_metrics"); break; } case SpecialRequestKind::kNone: break; } switch (grammar_execution_mode) { case GrammarExecutionMode::kJumpForward: { config.Set("grammar_execution_mode", "jump_forward"); break; } case GrammarExecutionMode::kConstraint: { config.Set("grammar_execution_mode", "constraint"); break; } } if (disagg_config.kind != DisaggRequestKind::kNone) { config.Set("disagg_config", disagg_config.AsJSON()); } return config; } /****************** GenerationConfig ******************/ Result GenerationConfig::Validate(GenerationConfig cfg) { using TResult = Result; if (cfg->n <= 0) { return TResult::Error("\"n\" should be at least 1"); } if (cfg->temperature < 0) { return TResult::Error("\"temperature\" should be non-negative"); } if (cfg->top_p < 0 || cfg->top_p > 1) { return TResult::Error("\"top_p\" should be in range [0, 1]"); } if (std::fabs(cfg->frequency_penalty) > 2.0) { return TResult::Error("frequency_penalty must be in [-2, 2]!"); } if (cfg->repetition_penalty <= 0) { return TResult::Error("\"repetition_penalty\" must be positive"); } if (cfg->top_logprobs < 0 || cfg->top_logprobs > 20) { return TResult::Error("At most 20 top logprob tokens are supported"); } if (cfg->top_logprobs != 0 && !(cfg->logprobs)) { return TResult::Error("\"logprobs\" must be true to support \"top_logprobs\""); } for (const auto& item : cfg->logit_bias) { double bias_value = item.second; if (std::fabs(bias_value) > 100.0) { return TResult::Error("Logit bias value should be in range [-100, 100]."); } } return TResult::Ok(cfg); } Result GenerationConfig::FromJSON(const tvm::ffi::json::Object& config, const GenerationConfig& default_config) { using TResult = Result; ObjectPtr n = tvm::ffi::make_object(); n->n = json::LookupOrDefault(config, "n", default_config->n); n->temperature = json::LookupOrDefault(config, "temperature", default_config->temperature); n->top_p = json::LookupOrDefault(config, "top_p", default_config->top_p); n->frequency_penalty = json::LookupOrDefault(config, "frequency_penalty", default_config->frequency_penalty); n->presence_penalty = json::LookupOrDefault(config, "presence_penalty", default_config->presence_penalty); n->repetition_penalty = json::LookupOrDefault(config, "repetition_penalty", default_config->repetition_penalty); n->logprobs = json::LookupOrDefault(config, "logprobs", default_config->logprobs); n->top_logprobs = json::LookupOrDefault(config, "top_logprobs", default_config->top_logprobs); std::optional logit_bias_obj = json::LookupOptional(config, "logit_bias"); if (logit_bias_obj.has_value()) { std::vector> logit_bias; logit_bias.reserve(logit_bias_obj.value().size()); for (auto [k, v] : logit_bias_obj.value()) { std::string token_id_str(k.cast()); TVM_FFI_ICHECK(v.try_cast().has_value()); double bias_value = v.cast(); logit_bias.emplace_back(std::stoi(token_id_str), bias_value); } n->logit_bias = std::move(logit_bias); } else { n->logit_bias = default_config->logit_bias; } n->seed = json::LookupOrDefault(config, "seed", std::random_device{}()); // "-1" means the generation will not stop until exceeding // model capability or hit any stop criteria. n->max_tokens = json::LookupOrDefault(config, "max_tokens", -1); std::optional stop_strs_arr = json::LookupOptional(config, "stop_strs"); if (stop_strs_arr.has_value()) { Array stop_strs; stop_strs.reserve(stop_strs_arr.value().size()); for (const auto& v : stop_strs_arr.value()) { if (!v.try_cast().has_value()) { return TResult::Error("Invalid stop string in stop_strs"); } stop_strs.push_back(v.cast()); } n->stop_strs = std::move(stop_strs); } else { n->stop_strs = default_config->stop_strs; } std::optional stop_token_ids_arr = json::LookupOptional(config, "stop_token_ids"); if (stop_token_ids_arr.has_value()) { std::vector stop_token_ids; stop_token_ids.reserve(stop_token_ids_arr.value().size()); for (const auto& v : stop_token_ids_arr.value()) { if (!v.try_cast().has_value()) { return TResult::Error("Invalid stop token in stop_token_ids"); } stop_token_ids.push_back(v.cast()); } n->stop_token_ids = std::move(stop_token_ids); } else { n->stop_token_ids = default_config->stop_token_ids; } std::optional response_format_obj = json::LookupOptional(config, "response_format"); if (response_format_obj.has_value()) { Result response_format_res = ResponseFormat::FromJSON(response_format_obj.value()); if (response_format_res.IsErr()) { return TResult::Error(response_format_res.UnwrapErr()); } n->response_format = response_format_res.Unwrap(); } else { n->response_format = default_config->response_format; } // "debug_config" is for internal usage. Not the part of OpenAI API spec. std::optional debug_config_obj = json::LookupOptional(config, "debug_config"); if (debug_config_obj.has_value()) { Result debug_config_res = DebugConfig::FromJSON(debug_config_obj.value()); if (debug_config_res.IsErr()) { return TResult::Error(debug_config_res.UnwrapErr()); } n->debug_config = debug_config_res.Unwrap(); } return Validate(GenerationConfig(n)); } GenerationConfig GenerationConfig::GetDefaultFromModelConfig( const tvm::ffi::json::Object& model_config_json) { ObjectPtr n = tvm::ffi::make_object(); n->max_tokens = -1; n->temperature = json::LookupOrDefault(model_config_json, "temperature", n->temperature); n->top_p = json::LookupOrDefault(model_config_json, "top_p", n->top_p); n->frequency_penalty = json::LookupOrDefault(model_config_json, "frequency_penalty", n->frequency_penalty); n->presence_penalty = json::LookupOrDefault(model_config_json, "presence_penalty", n->presence_penalty); return GenerationConfig(n); } tvm::ffi::json::Object GenerationConfigNode::AsJSON() const { tvm::ffi::json::Object config; config.Set("n", static_cast(this->n)); config.Set("temperature", this->temperature); config.Set("top_p", this->top_p); config.Set("frequency_penalty", this->frequency_penalty); config.Set("presence_penalty", this->presence_penalty); config.Set("repetition_penalty", this->repetition_penalty); config.Set("logprobs", this->logprobs); config.Set("top_logprobs", static_cast(this->top_logprobs)); config.Set("max_tokens", static_cast(this->max_tokens)); config.Set("seed", static_cast(this->seed)); tvm::ffi::json::Object logit_bias_obj; for (auto [token_id, bias] : logit_bias) { logit_bias_obj.Set(std::to_string(token_id), static_cast(bias)); } config.Set("logit_bias", logit_bias_obj); tvm::ffi::json::Array stop_strs_arr; for (String stop_str : this->stop_strs) { stop_strs_arr.push_back(stop_str); } config.Set("stop_strs", stop_strs_arr); tvm::ffi::json::Array stop_token_ids_arr; for (int stop_token_id : this->stop_token_ids) { stop_token_ids_arr.push_back(static_cast(stop_token_id)); } config.Set("stop_token_ids", stop_token_ids_arr); tvm::ffi::json::Object response_format; response_format.Set("type", this->response_format.type); if (this->response_format.schema) { response_format.Set("schema", this->response_format.schema.value()); } else { response_format.Set("schema", tvm::Any(nullptr)); } config.Set("response_format", response_format); config.Set("debug_config", debug_config.AsJSON()); return config; } /****************** EngineConfig ******************/ EngineConfig EngineConfig::FromJSONAndInferredConfig( const tvm::ffi::json::Object& json, const InferrableEngineConfig& inferred_config) { TVM_FFI_ICHECK(inferred_config.max_num_sequence.has_value()); TVM_FFI_ICHECK(inferred_config.max_total_sequence_length.has_value()); TVM_FFI_ICHECK(inferred_config.prefill_chunk_size.has_value()); TVM_FFI_ICHECK(inferred_config.max_history_size.has_value()); ObjectPtr n = tvm::ffi::make_object(); // - Get models and model libs. n->model = json::Lookup(json, "model"); n->model_lib = json::Lookup(json, "model_lib"); std::vector additional_models; std::vector additional_model_libs; tvm::ffi::json::Array additional_models_arr = json::LookupOrDefault( json, "additional_models", tvm::ffi::json::Array()); int num_additional_models = additional_models_arr.size(); additional_models.reserve(num_additional_models); additional_model_libs.reserve(num_additional_models); for (int i = 0; i < num_additional_models; ++i) { tvm::ffi::json::Array additional_model_pair = json::Lookup(additional_models_arr, i); additional_models.push_back(json::Lookup(additional_model_pair, 0)); additional_model_libs.push_back(json::Lookup(additional_model_pair, 1)); } n->additional_models = additional_models; n->additional_model_libs = additional_model_libs; n->mode = EngineModeFromString(json::Lookup(json, "mode")); // - Other fields with default value. n->gpu_memory_utilization = static_cast( json::LookupOrDefault(json, "gpu_memory_utilization", n->gpu_memory_utilization)); n->kv_cache_page_size = static_cast( json::LookupOrDefault(json, "kv_cache_page_size", n->kv_cache_page_size)); n->speculative_mode = SpeculativeModeFromString(json::LookupOrDefault( json, "speculative_mode", SpeculativeModeToString(n->speculative_mode))); n->spec_draft_length = static_cast( json::LookupOrDefault(json, "spec_draft_length", n->spec_draft_length)); n->spec_tree_width = static_cast(json::LookupOrDefault(json, "spec_tree_width", n->spec_tree_width)); n->prefill_mode = PrefillModeFromString(json::LookupOrDefault( json, "prefill_mode", PrefillModeToString(n->prefill_mode))); n->verbose = json::LookupOrDefault(json, "verbose", n->verbose); // - Fields from the inferred engine config. n->max_num_sequence = inferred_config.max_num_sequence.value(); n->max_total_sequence_length = inferred_config.max_total_sequence_length.value(); if (inferred_config.max_single_sequence_length.has_value()) { n->max_single_sequence_length = inferred_config.max_single_sequence_length.value(); } n->prefill_chunk_size = inferred_config.prefill_chunk_size.value(); n->max_history_size = inferred_config.max_history_size.value(); n->prefix_cache_mode = PrefixCacheModeFromString(json::LookupOrDefault( json, "prefix_cache_mode", PrefixCacheModeToString(n->prefix_cache_mode))); n->prefix_cache_max_num_recycling_seqs = static_cast(json::LookupOrDefault( json, "prefix_cache_max_num_recycling_seqs", n->max_num_sequence)); return EngineConfig(n); } Result>> EngineConfig::GetModelsAndModelLibsFromJSONString(const std::string& json_str) { using TResult = Result>>; tvm::ffi::String err; auto config_json = tvm::ffi::json::Parse(json_str, &err); if (!err.empty()) { return TResult::Error(err); } // Get the models and model libs from JSON. tvm::ffi::json::Object config = config_json.cast(); String model = json::Lookup(config, "model"); String model_lib = json::Lookup(config, "model_lib"); tvm::ffi::json::Array additional_models_arr = json::LookupOrDefault( config, "additional_models", tvm::ffi::json::Array()); int num_additional_models = additional_models_arr.size(); std::vector> models_and_model_libs; models_and_model_libs.reserve(num_additional_models + 1); models_and_model_libs.emplace_back(model, model_lib); for (int i = 0; i < num_additional_models; ++i) { tvm::ffi::json::Array additional_model_pair = json::Lookup(additional_models_arr, i); models_and_model_libs.emplace_back(json::Lookup(additional_model_pair, 0), json::Lookup(additional_model_pair, 1)); } return TResult::Ok(models_and_model_libs); } String EngineConfigNode::AsJSONString() const { tvm::ffi::json::Object config; // - Models and model libs config.Set("model", this->model); config.Set("model_lib", this->model_lib); tvm::ffi::json::Array additional_models_arr; additional_models_arr.reserve(this->additional_models.size()); for (int i = 0; i < static_cast(this->additional_models.size()); ++i) { tvm::ffi::json::Array pair; pair.push_back(this->additional_models[i]); pair.push_back(this->additional_model_libs[i]); additional_models_arr.push_back(pair); } config.Set("additional_models", additional_models_arr); // - Other fields config.Set("mode", EngineModeToString(this->mode)); config.Set("gpu_memory_utilization", static_cast(this->gpu_memory_utilization)); config.Set("kv_cache_page_size", static_cast(this->kv_cache_page_size)); config.Set("max_num_sequence", static_cast(this->max_num_sequence)); config.Set("max_total_sequence_length", static_cast(this->max_total_sequence_length)); config.Set("max_single_sequence_length", static_cast(this->max_single_sequence_length)); config.Set("prefill_chunk_size", static_cast(this->prefill_chunk_size)); config.Set("max_history_size", static_cast(this->max_history_size)); config.Set("prefix_cache_mode", PrefixCacheModeToString(this->prefix_cache_mode)); config.Set("prefix_cache_max_num_recycling_seqs", static_cast(this->prefix_cache_max_num_recycling_seqs)); config.Set("speculative_mode", SpeculativeModeToString(this->speculative_mode)); config.Set("spec_draft_length", static_cast(this->spec_draft_length)); config.Set("prefill_mode", PrefillModeToString(this->prefill_mode)); config.Set("verbose", static_cast(this->verbose)); return tvm::ffi::json::Stringify(config, 2); } /****************** InferrableEngineConfig ******************/ /*! \brief The class for config limitation from models. */ struct ModelConfigLimits { int64_t model_compile_time_max_single_sequence_length; int64_t model_runtime_max_single_sequence_length; int64_t model_compile_time_max_prefill_chunk_size; int64_t model_runtime_max_prefill_chunk_size; int64_t model_max_sliding_window_size; int64_t model_max_batch_size; }; /*! \brief Convert the bytes to megabytes, keeping 3 decimals. */ inline std::string BytesToMegabytesString(double bytes) { std::ostringstream os; os << std::setprecision(3) << std::fixed << (bytes / 1024 / 1024); return os.str(); } /*! * \brief Get the upper bound of single sequence length, prefill size and batch size * from model config. */ Result GetModelConfigLimits( const std::vector& model_configs, const std::vector& model_metadata) { TVM_FFI_ICHECK_EQ(model_configs.size(), model_metadata.size()); int64_t model_compile_time_max_single_sequence_length = std::numeric_limits::max(); int64_t model_runtime_max_single_sequence_length = std::numeric_limits::max(); int64_t model_compile_time_max_prefill_chunk_size = std::numeric_limits::max(); int64_t model_runtime_max_prefill_chunk_size = std::numeric_limits::max(); int64_t model_max_batch_size = std::numeric_limits::max(); int64_t model_max_sliding_window_size = std::numeric_limits::max(); for (int i = 0; i < static_cast(model_configs.size()); ++i) { // - The maximum single sequence length is the minimum context window size among all models. int64_t runtime_context_window_size = json::LookupOptional(model_configs[i], "context_window_size").value_or(-1); int64_t compile_time_context_window_size = model_metadata[i].context_window_size; // limit runtime setting by compile time setting if (compile_time_context_window_size != -1) { if (runtime_context_window_size == -1 || runtime_context_window_size > compile_time_context_window_size) { runtime_context_window_size = compile_time_context_window_size; } } if (compile_time_context_window_size != -1) { model_compile_time_max_single_sequence_length = std::min(model_compile_time_max_single_sequence_length, compile_time_context_window_size); } if (runtime_context_window_size != -1) { model_runtime_max_single_sequence_length = std::min(model_runtime_max_single_sequence_length, runtime_context_window_size); } // - The maximum prefill chunk size is the minimum prefill chunk size among all models. int64_t runtime_prefill_chunk_size = json::Lookup(model_configs[i], "prefill_chunk_size"); int64_t compile_time_prefill_chunk_size = model_metadata[i].prefill_chunk_size; // limit runtime setting by compile time setting if (compile_time_prefill_chunk_size != -1) { if (runtime_prefill_chunk_size == -1 || runtime_prefill_chunk_size > compile_time_prefill_chunk_size) { runtime_prefill_chunk_size = compile_time_prefill_chunk_size; } } if (compile_time_prefill_chunk_size != -1) { model_compile_time_max_prefill_chunk_size = std::min(model_compile_time_max_prefill_chunk_size, compile_time_prefill_chunk_size); } if (runtime_prefill_chunk_size != -1) { model_runtime_max_prefill_chunk_size = std::min(model_runtime_max_prefill_chunk_size, runtime_prefill_chunk_size); } // - The maximum batch size is the minimum max batch size among all models. model_max_batch_size = std::min(model_max_batch_size, model_metadata[i].max_batch_size); // - The maximum sliding window size is the minimum among all models. int64_t runtime_sliding_window_size = json::LookupOptional(model_configs[i], "sliding_window_size").value_or(-1); if (runtime_sliding_window_size != -1) { model_max_sliding_window_size = std::min(model_max_sliding_window_size, runtime_sliding_window_size); } } TVM_FFI_ICHECK_NE(model_compile_time_max_prefill_chunk_size, std::numeric_limits::max()); TVM_FFI_ICHECK_NE(model_runtime_max_prefill_chunk_size, std::numeric_limits::max()); TVM_FFI_ICHECK_NE(model_max_batch_size, std::numeric_limits::max()); TVM_FFI_ICHECK_GT(model_compile_time_max_prefill_chunk_size, 0); TVM_FFI_ICHECK_GT(model_runtime_max_prefill_chunk_size, 0); TVM_FFI_ICHECK_GT(model_max_batch_size, 0); return Result::Ok( {model_compile_time_max_single_sequence_length, model_runtime_max_single_sequence_length, model_compile_time_max_prefill_chunk_size, model_runtime_max_prefill_chunk_size, model_max_sliding_window_size, model_max_batch_size}); } /*! \brief The class for memory usage estimation result. */ struct MemUsageEstimationResult { double total_memory_bytes; double kv_cache_memory_bytes; double temp_memory_bytes; InferrableEngineConfig inferred_config; }; Result EstimateMemoryUsageOnMode( EngineMode mode, Device device, double gpu_memory_utilization, int64_t params_bytes, int64_t temp_buffer_bytes, const std::vector& model_configs, // const std::vector& model_metadata, // ModelConfigLimits model_config_limits, // InferrableEngineConfig init_config, bool verbose) { std::ostringstream os; InferrableEngineConfig inferred_config = init_config; // - 1. max_num_sequence if (!init_config.max_num_sequence.has_value()) { if (mode == EngineMode::kLocal) { inferred_config.max_num_sequence = std::min(static_cast(4), model_config_limits.model_max_batch_size); } else if (mode == EngineMode::kInteractive) { inferred_config.max_num_sequence = 1; } else { inferred_config.max_num_sequence = model_config_limits.model_max_batch_size; } os << "max batch size will be set to " << inferred_config.max_num_sequence.value() << ", "; } else { os << "max batch size " << inferred_config.max_num_sequence.value() << " is specified by user, "; } int64_t max_num_sequence = inferred_config.max_num_sequence.value(); // - 2. max_single_sequence_length if (!init_config.max_single_sequence_length.has_value()) { inferred_config.max_single_sequence_length = model_config_limits.model_runtime_max_single_sequence_length; } else { inferred_config.max_single_sequence_length = std::min(inferred_config.max_single_sequence_length.value(), model_config_limits.model_compile_time_max_single_sequence_length); } // - 3. infer the maximum total sequence length that can fit GPU memory. double kv_bytes_per_token = 0; double kv_aux_workspace_bytes = 0; double model_workspace_bytes = 0; double logit_processor_workspace_bytes = 0; TVM_FFI_ICHECK_EQ(model_configs.size(), model_metadata.size()); int num_models = model_configs.size(); for (int i = 0; i < num_models; ++i) { // - Read the vocab size and compile-time prefill chunk size (which affects memory allocation). tvm::ffi::json::Object compile_time_model_config = json::Lookup(model_configs[i], "model_config"); int64_t vocab_size = json::Lookup(compile_time_model_config, "vocab_size"); int64_t prefill_chunk_size = json::Lookup(compile_time_model_config, "prefill_chunk_size"); // - Calculate KV cache memory usage. int64_t num_layers = model_metadata[i].kv_cache_metadata.num_hidden_layers; int64_t head_dim = model_metadata[i].kv_cache_metadata.head_dim; int64_t num_qo_heads = model_metadata[i].kv_cache_metadata.num_attention_heads; int64_t num_kv_heads = model_metadata[i].kv_cache_metadata.num_key_value_heads; int64_t hidden_size = head_dim * num_qo_heads; kv_bytes_per_token += head_dim * num_kv_heads * (num_layers / model_metadata[i].pipeline_parallel_stages) * 4 + 1.25; kv_aux_workspace_bytes += (max_num_sequence + 1) * 88 + prefill_chunk_size * (num_qo_heads + 1) * 8 + prefill_chunk_size * head_dim * (num_qo_heads + num_kv_heads) * 4 + 48 * 1024 * 1024; model_workspace_bytes += prefill_chunk_size * 4 + max_num_sequence * 4 + (prefill_chunk_size * 2 + max_num_sequence) * hidden_size * 2; logit_processor_workspace_bytes += max_num_sequence * 20 + max_num_sequence * vocab_size * 16.125; } int64_t gpu_size_bytes = TotalDetectGlobalMemory(device); // Compute the maximum total sequence length under the GPU memory budget. int64_t model_max_total_sequence_length = static_cast((gpu_size_bytes * gpu_memory_utilization // - params_bytes // - temp_buffer_bytes // - kv_aux_workspace_bytes // - model_workspace_bytes // - logit_processor_workspace_bytes) / kv_bytes_per_token); if (model_max_total_sequence_length <= 0) { if (verbose) { LOG(INFO) << "temp_buffer = " << BytesToMegabytesString(temp_buffer_bytes); LOG(INFO) << "kv_aux workspace = " << BytesToMegabytesString(kv_aux_workspace_bytes); LOG(INFO) << "model workspace = " << BytesToMegabytesString(model_workspace_bytes); LOG(INFO) << "logit processor workspace = " << BytesToMegabytesString(logit_processor_workspace_bytes); } return Result::Error( "Insufficient GPU memory error: " "The available single GPU memory is " + BytesToMegabytesString(gpu_size_bytes * gpu_memory_utilization) + " MB, " "which is less than the sum of model weight size (" + BytesToMegabytesString(params_bytes) + " MB) and temporary buffer size (" + BytesToMegabytesString(temp_buffer_bytes + kv_aux_workspace_bytes + model_workspace_bytes + logit_processor_workspace_bytes) + " MB).\n" "1. You can set a larger \"gpu_memory_utilization\" value.\n" "2. If the model weight size is too large, please enable tensor parallelism by passing " "`--tensor-parallel-shards $NGPU` to `mlc_llm gen_config` or use quantization.\n" "3. If the temporary buffer size is too large, please use a smaller `--prefill-chunk-size` " "in `mlc_llm gen_config`."); } if (device.device_type == DLDeviceType::kDLMetal) { // NOTE: Metal runtime has severe performance issues with large buffers. // To work around the issue, we limit the KV cache capacity to 32768. model_max_total_sequence_length = std::min(model_max_total_sequence_length, static_cast(32768)); } // Compute the total memory usage except the KV cache part. double total_mem_usage_except_kv_cache = (params_bytes + temp_buffer_bytes + kv_aux_workspace_bytes + model_workspace_bytes + logit_processor_workspace_bytes); // - 4. max_total_sequence_length if (!init_config.max_total_sequence_length.has_value()) { if (mode == EngineMode::kLocal) { inferred_config.max_total_sequence_length = std::min( {model_max_total_sequence_length, inferred_config.max_single_sequence_length.value(), static_cast(8192)}); } else if (mode == EngineMode::kInteractive) { inferred_config.max_total_sequence_length = std::min( {model_max_total_sequence_length, inferred_config.max_single_sequence_length.value()}); } else { inferred_config.max_total_sequence_length = inferred_config.max_single_sequence_length.value() == std::numeric_limits::max() ? model_max_total_sequence_length : std::min(model_max_total_sequence_length, max_num_sequence * inferred_config.max_single_sequence_length.value()); } os << "max KV cache token capacity will be set to " << inferred_config.max_total_sequence_length.value() << ", "; } else { os << "max KV cache token capacity " << inferred_config.max_total_sequence_length.value() << " is specified by user, "; } // - 5. prefill_chunk_size if (!init_config.prefill_chunk_size.has_value()) { if (mode == EngineMode::kLocal || mode == EngineMode::kInteractive) { inferred_config.prefill_chunk_size = std::min({model_config_limits.model_runtime_max_prefill_chunk_size, inferred_config.max_total_sequence_length.value(), inferred_config.max_single_sequence_length.value()}); } else { inferred_config.prefill_chunk_size = model_config_limits.model_runtime_max_prefill_chunk_size; } os << "prefill chunk size will be set to " << inferred_config.prefill_chunk_size.value() << ". "; } else { os << "prefill chunk size " << inferred_config.prefill_chunk_size.value() << " is specified by user. "; } // - Print logging message if (verbose) { LOG(INFO) << "Under mode \"" << EngineModeToString(mode) << "\", " << os.str(); } return Result::Ok( {total_mem_usage_except_kv_cache + inferred_config.max_total_sequence_length.value() * kv_bytes_per_token, kv_bytes_per_token * inferred_config.max_total_sequence_length.value() + kv_aux_workspace_bytes, model_workspace_bytes + logit_processor_workspace_bytes + temp_buffer_bytes, inferred_config}); } Result InferrableEngineConfig::InferForKVCache( EngineMode mode, Device device, double gpu_memory_utilization, const std::vector& model_configs, const std::vector& model_metadata, InferrableEngineConfig init_config, bool verbose) { // - Check if max_history_size is not set. if (init_config.max_history_size.has_value() && init_config.max_history_size.value() != 0) { return Result::Error( "KV cache does not support max_history_size, while it is set to " + std::to_string(init_config.max_history_size.value()) + " in the input EngineConfig"); } // - Get the upper bound of single sequence length, prefill size and batch size // from model config. Result model_config_limits_res = GetModelConfigLimits(model_configs, model_metadata); if (model_config_limits_res.IsErr()) { return Result::Error(model_config_limits_res.UnwrapErr()); } ModelConfigLimits model_config_limits = model_config_limits_res.Unwrap(); // - Get total model parameter size and temporary in-function buffer // size in bytes on single GPU. int64_t params_bytes = 0; int64_t temp_buffer_bytes = 0; for (const ModelMetadata& metadata : model_metadata) { for (const ModelMetadata::Param& param : metadata.params) { int64_t param_size = (param.dtype.bits * param.dtype.lanes + 7) / 8; for (int64_t v : param.shape) { TVM_FFI_ICHECK_GE(v, 0); param_size *= v; } params_bytes += param_size; } params_bytes /= metadata.pipeline_parallel_stages; for (const auto& [func_name, temp_buffer_size] : metadata.memory_usage) { temp_buffer_bytes = std::max(temp_buffer_bytes, temp_buffer_size); } } // Magnify the temp buffer by a factor of 2 for safety. temp_buffer_bytes *= 2; // - Infer the engine config and estimate memory usage for each mode. Result local_mode_estimation_result = EstimateMemoryUsageOnMode( EngineMode::kLocal, device, gpu_memory_utilization, params_bytes, temp_buffer_bytes, model_configs, model_metadata, model_config_limits, init_config, verbose); Result interactive_mode_estimation_result = EstimateMemoryUsageOnMode( EngineMode::kInteractive, device, gpu_memory_utilization, params_bytes, temp_buffer_bytes, model_configs, model_metadata, model_config_limits, init_config, verbose); Result server_mode_estimation_result = EstimateMemoryUsageOnMode( EngineMode::kServer, device, gpu_memory_utilization, params_bytes, temp_buffer_bytes, model_configs, model_metadata, model_config_limits, init_config, verbose); // - Pick the estimation result according to the mode. std::string mode_name; Result final_estimation_result; if (mode == EngineMode::kLocal) { final_estimation_result = std::move(local_mode_estimation_result); } else if (mode == EngineMode::kInteractive) { final_estimation_result = std::move(interactive_mode_estimation_result); } else { final_estimation_result = std::move(server_mode_estimation_result); } if (final_estimation_result.IsErr()) { return Result::Error(final_estimation_result.UnwrapErr()); } // - Print log message. MemUsageEstimationResult final_estimation = final_estimation_result.Unwrap(); InferrableEngineConfig inferred_config = std::move(final_estimation.inferred_config); if (verbose) { LOG(INFO) << "The actual engine mode is \"" << EngineModeToString(mode) << "\". So max batch size is " << inferred_config.max_num_sequence.value() << ", max KV cache token capacity is " << inferred_config.max_total_sequence_length.value() << ", prefill chunk size is " << inferred_config.prefill_chunk_size.value() << "."; LOG(INFO) << "Estimated total single GPU memory usage: " << BytesToMegabytesString(final_estimation.total_memory_bytes) << " MB (Parameters: " << BytesToMegabytesString(params_bytes) << " MB. KVCache: " << BytesToMegabytesString(final_estimation.kv_cache_memory_bytes) << " MB. Temporary buffer: " << BytesToMegabytesString(final_estimation.temp_memory_bytes) << " MB). The actual usage might be slightly larger than the estimated number."; } inferred_config.max_history_size = 0; return Result::Ok(inferred_config); } Result InferrableEngineConfig::InferForRNNState( EngineMode mode, Device device, double gpu_memory_utilization, const std::vector& model_configs, const std::vector& model_metadata, InferrableEngineConfig init_config, bool verbose) { // - Check max_single_sequence_length is not set. if (init_config.max_single_sequence_length.has_value()) { return Result::Error( "RNN state does not support max_single_sequence_length, while it is set to " + std::to_string(init_config.max_single_sequence_length.value()) + " in the input EngineConfig"); } // - Get the upper bound of single sequence length, prefill size and batch size // from model config. Result model_config_limits_res = GetModelConfigLimits(model_configs, model_metadata); if (model_config_limits_res.IsErr()) { return Result::Error(model_config_limits_res.UnwrapErr()); } ModelConfigLimits model_config_limits = model_config_limits_res.Unwrap(); std::ostringstream os; InferrableEngineConfig inferred_config = init_config; // - 1. prefill_chunk_size if (!init_config.prefill_chunk_size.has_value()) { inferred_config.prefill_chunk_size = std::min( model_config_limits.model_runtime_max_prefill_chunk_size, static_cast(4096)); os << "prefill chunk size will be set to " << inferred_config.prefill_chunk_size.value() << ", "; } else { os << "prefill chunk size " << inferred_config.prefill_chunk_size.value() << " is specified by user, "; } // - 2. max_batch_size if (!init_config.max_num_sequence.has_value()) { inferred_config.max_num_sequence = mode == EngineMode::kInteractive ? 1 : std::min(static_cast(4), model_config_limits.model_max_batch_size); os << "max batch size will be set to " << inferred_config.max_num_sequence.value() << ", "; } else { os << "max batch size " << inferred_config.max_num_sequence.value() << " is specified by user, "; } int64_t max_num_sequence = inferred_config.max_num_sequence.value(); // - 3. max_total_sequence_length if (!init_config.max_total_sequence_length.has_value()) { inferred_config.max_total_sequence_length = 32768; os << "max RNN state token capacity will be set to " << inferred_config.max_total_sequence_length.value() << ". "; } else { os << "max RNN state token capacity " << inferred_config.max_total_sequence_length.value() << " is specified by user. "; } // - Extra logging message if (mode == EngineMode::kLocal) { os << "We choose small max batch size and RNN state capacity to use less GPU memory."; } else if (mode == EngineMode::kInteractive) { os << "We fix max batch size to 1 for interactive single sequence use."; } else { os << "We use as much GPU memory as possible (within the limit of gpu_memory_utilization)."; } if (verbose) { LOG(INFO) << "Under mode \"" << EngineModeToString(mode) << "\", " << os.str(); } // - Get total model parameter size and temporary in-function buffer // size in bytes on single GPU. int64_t params_bytes = 0; int64_t temp_buffer_bytes = 0; for (const ModelMetadata& metadata : model_metadata) { for (const ModelMetadata::Param& param : metadata.params) { int64_t param_size = (param.dtype.bits * param.dtype.lanes + 7) / 8; for (int64_t v : param.shape) { TVM_FFI_ICHECK_GE(v, 0); param_size *= v; } params_bytes += param_size; } for (const auto& [func_name, temp_buffer_size] : metadata.memory_usage) { temp_buffer_bytes += temp_buffer_size; } } // - 4. max_history_size double rnn_state_base_bytes = 0; // The memory usage for rnn state when history = 1. double model_workspace_bytes = 0; double logit_processor_workspace_bytes = 0; TVM_FFI_ICHECK_EQ(model_configs.size(), model_metadata.size()); int num_models = model_configs.size(); for (int i = 0; i < num_models; ++i) { // - Read the vocab size and compile-time prefill chunk size (which affects memory allocation). tvm::ffi::json::Object compile_time_model_config = json::Lookup(model_configs[i], "model_config"); int64_t vocab_size = json::Lookup(compile_time_model_config, "vocab_size"); int64_t prefill_chunk_size = json::Lookup(compile_time_model_config, "prefill_chunk_size"); int64_t head_size = json::Lookup(compile_time_model_config, "head_size"); int64_t num_heads = json::Lookup(compile_time_model_config, "num_heads"); int64_t num_layers = json::Lookup(compile_time_model_config, "num_hidden_layers"); int64_t hidden_size = json::Lookup(compile_time_model_config, "hidden_size"); // - Calculate RNN state memory usage. rnn_state_base_bytes += (max_num_sequence * hidden_size * num_layers * 2 * 2 + max_num_sequence * num_heads * head_size * head_size * num_layers * 2); model_workspace_bytes += prefill_chunk_size * 4 + max_num_sequence * 4 + (prefill_chunk_size * 2 + max_num_sequence) * hidden_size * 2; logit_processor_workspace_bytes += max_num_sequence * 20 + max_num_sequence * vocab_size * 16.125; } int64_t gpu_size_bytes = TotalDetectGlobalMemory(device); // Compute the maximum history size length under the GPU memory budget. int64_t model_max_history_size = static_cast((gpu_size_bytes * gpu_memory_utilization // - params_bytes // - temp_buffer_bytes // - model_workspace_bytes // - logit_processor_workspace_bytes) / rnn_state_base_bytes); if (model_max_history_size <= 0) { return Result::Error( "Insufficient GPU memory error: " "The available single GPU memory is " + BytesToMegabytesString(gpu_size_bytes * gpu_memory_utilization) + " MB, " "which is less than the sum of model weight size (" + BytesToMegabytesString(params_bytes) + " MB) and temporary buffer size (" + BytesToMegabytesString( (temp_buffer_bytes + model_workspace_bytes + logit_processor_workspace_bytes)) + " MB). " "If the model weight size is too large, please use quantization. " "If the temporary buffer size is too large, please use a smaller `--prefill-chunk-size` in " "`mlc_llm gen_config`."); } if (!init_config.max_history_size.has_value()) { inferred_config.max_history_size = model_max_history_size; } else { inferred_config.max_history_size = std::min(inferred_config.max_history_size.value(), model_max_history_size); } if (verbose) { LOG(INFO) << "The actual engine mode is \"" << EngineModeToString(mode) << "\". So max batch size is " << inferred_config.max_num_sequence.value() << ", max RNN state token capacity is " << inferred_config.max_total_sequence_length.value() << ", prefill chunk size is " << inferred_config.prefill_chunk_size.value() << "."; LOG(INFO) << "Estimated total single GPU memory usage: " << BytesToMegabytesString(params_bytes + temp_buffer_bytes + inferred_config.max_history_size.value() * rnn_state_base_bytes) << " MB (Parameters: " << BytesToMegabytesString(params_bytes) << " MB. RNN state: " << BytesToMegabytesString(inferred_config.max_history_size.value() * rnn_state_base_bytes) << " MB. Temporary buffer: " << BytesToMegabytesString(model_workspace_bytes + logit_processor_workspace_bytes + temp_buffer_bytes) << " MB). The actual usage might be slightly larger than the estimated number."; } return Result::Ok(inferred_config); } /****************** Config utils ******************/ Result ModelsUseKVCache(const std::vector& model_configs) { TVM_FFI_ICHECK_GE(model_configs.size(), 1); std::string model_type = json::Lookup(model_configs[0], "model_type"); bool use_kv_cache = model_type.find("rwkv") == std::string::npos; for (int i = 1; i < static_cast(model_configs.size()); ++i) { if ((json::Lookup(model_configs[i], "model_type").find("rwkv") == std::string::npos) != use_kv_cache) { return Result::Error( "Invalid models in EngineConfig. Models must be all RNN model or none model is RNN " "model."); } } return Result::Ok(use_kv_cache); } } // namespace serve } // namespace llm } // namespace mlc