chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
Build Docs / Deploy Docs (push) Has been cancelled
Windows CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:23:58 +08:00
commit 770d92cb1f
694 changed files with 114634 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file base.h
*/
#ifndef MLC_LLM_DLL
#ifdef _WIN32
#ifdef MLC_LLM_EXPORTS
#define MLC_LLM_DLL __declspec(dllexport)
#else
#define MLC_LLM_DLL __declspec(dllimport)
#endif
#else
#define MLC_LLM_DLL __attribute__((visibility("default")))
#endif
#endif
+618
View File
@@ -0,0 +1,618 @@
#include "conv_template.h"
#include <tvm/ffi/function.h>
#include "../support/json_parser.h"
#include "image_utils.h"
namespace mlc {
namespace llm {
namespace json_ffi {
using namespace mlc::llm;
/****************** Model vision config ******************/
ModelVisionConfig ModelVisionConfig::FromJSON(const tvm::ffi::json::Object& json_obj) {
ModelVisionConfig config;
Result<int64_t> hidden_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "hidden_size");
if (hidden_size_res.IsOk()) {
config.hidden_size = static_cast<int>(hidden_size_res.Unwrap());
}
Result<int64_t> image_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "image_size");
if (image_size_res.IsOk()) {
config.image_size = static_cast<int>(image_size_res.Unwrap());
}
Result<int64_t> intermediate_size_res =
json::LookupWithResultReturn<int64_t>(json_obj, "intermediate_size");
if (intermediate_size_res.IsOk()) {
config.intermediate_size = static_cast<int>(intermediate_size_res.Unwrap());
}
Result<int64_t> num_attention_heads_res =
json::LookupWithResultReturn<int64_t>(json_obj, "num_attention_heads");
if (num_attention_heads_res.IsOk()) {
config.num_attention_heads = static_cast<int>(num_attention_heads_res.Unwrap());
}
Result<int64_t> num_hidden_layers_res =
json::LookupWithResultReturn<int64_t>(json_obj, "num_hidden_layers");
if (num_hidden_layers_res.IsOk()) {
config.num_hidden_layers = static_cast<int>(num_hidden_layers_res.Unwrap());
}
Result<int64_t> patch_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "patch_size");
if (patch_size_res.IsOk()) {
config.patch_size = static_cast<int>(patch_size_res.Unwrap());
}
Result<int64_t> projection_dim_res =
json::LookupWithResultReturn<int64_t>(json_obj, "projection_dim");
if (projection_dim_res.IsOk()) {
config.projection_dim = static_cast<int>(projection_dim_res.Unwrap());
}
Result<int64_t> vocab_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "vocab_size");
if (vocab_size_res.IsOk()) {
config.vocab_size = static_cast<int>(vocab_size_res.Unwrap());
}
Result<std::string> dtype_res = json::LookupWithResultReturn<std::string>(json_obj, "dtype");
if (dtype_res.IsOk()) {
config.dtype = dtype_res.Unwrap();
}
Result<int64_t> num_channels_res =
json::LookupWithResultReturn<int64_t>(json_obj, "num_channels");
if (num_channels_res.IsOk()) {
config.num_channels = static_cast<int>(num_channels_res.Unwrap());
}
Result<double> layer_norm_eps_res =
json::LookupWithResultReturn<double>(json_obj, "layer_norm_eps");
if (layer_norm_eps_res.IsOk()) {
config.layer_norm_eps = layer_norm_eps_res.Unwrap();
}
return config;
}
/****************** Model config ******************/
ModelConfig ModelConfig::FromJSON(const tvm::ffi::json::Object& json_obj) {
ModelConfig config;
Result<int64_t> vocab_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "vocab_size");
if (vocab_size_res.IsOk()) {
config.vocab_size = static_cast<int>(vocab_size_res.Unwrap());
}
Result<int64_t> context_window_size_res =
json::LookupWithResultReturn<int64_t>(json_obj, "context_window_size");
if (context_window_size_res.IsOk()) {
config.context_window_size = static_cast<int>(context_window_size_res.Unwrap());
}
Result<int64_t> sliding_window_size_res =
json::LookupWithResultReturn<int64_t>(json_obj, "sliding_window_size");
if (sliding_window_size_res.IsOk()) {
config.sliding_window_size = static_cast<int>(sliding_window_size_res.Unwrap());
}
Result<int64_t> prefill_chunk_size_res =
json::LookupWithResultReturn<int64_t>(json_obj, "prefill_chunk_size");
if (prefill_chunk_size_res.IsOk()) {
config.prefill_chunk_size = static_cast<int>(prefill_chunk_size_res.Unwrap());
}
Result<int64_t> tensor_parallel_shards_res =
json::LookupWithResultReturn<int64_t>(json_obj, "tensor_parallel_shards");
if (tensor_parallel_shards_res.IsOk()) {
config.tensor_parallel_shards = static_cast<int>(tensor_parallel_shards_res.Unwrap());
}
Result<int64_t> pipeline_parallel_stages_res =
json::LookupWithResultReturn<int64_t>(json_obj, "pipeline_parallel_stages");
if (pipeline_parallel_stages_res.IsOk()) {
config.pipeline_parallel_stages = static_cast<int>(pipeline_parallel_stages_res.Unwrap());
}
Result<int64_t> max_batch_size_res =
json::LookupWithResultReturn<int64_t>(json_obj, "max_batch_size");
if (max_batch_size_res.IsOk()) {
config.max_batch_size = static_cast<int>(max_batch_size_res.Unwrap());
}
if (json_obj.count("vision_config")) {
const tvm::ffi::json::Object& vision_config_obj =
json_obj.at("vision_config").cast<tvm::ffi::json::Object>();
config.vision_config = ModelVisionConfig::FromJSON(vision_config_obj);
}
return config;
}
/****************** Conversation template ******************/
std::unordered_map<MessagePlaceholders, std::string> PLACEHOLDERS = {
{MessagePlaceholders::SYSTEM, "{system_message}"},
{MessagePlaceholders::USER, "{user_message}"},
{MessagePlaceholders::ASSISTANT, "{assistant_message}"},
{MessagePlaceholders::TOOL, "{tool_message}"},
{MessagePlaceholders::FUNCTION, "{function_string}"}};
MessagePlaceholders MessagePlaceholderFromString(const std::string& role) {
static const std::unordered_map<std::string, MessagePlaceholders> enum_map = {
{"system", MessagePlaceholders::SYSTEM}, {"user", MessagePlaceholders::USER},
{"assistant", MessagePlaceholders::ASSISTANT}, {"tool", MessagePlaceholders::TOOL},
{"function", MessagePlaceholders::FUNCTION},
};
return enum_map.at(role);
}
Conversation::Conversation()
: role_templates({{"user", PLACEHOLDERS[MessagePlaceholders::USER]},
{"assistant", PLACEHOLDERS[MessagePlaceholders::ASSISTANT]},
{"tool", PLACEHOLDERS[MessagePlaceholders::TOOL]}}) {}
std::string Conversation::GetSystemText(const std::string& system_msg) const {
std::string system_text = this->system_template;
static std::string system_placeholder = PLACEHOLDERS[MessagePlaceholders::SYSTEM];
size_t pos = system_text.find(system_placeholder);
if (pos != std::string::npos) {
system_text.replace(pos, system_placeholder.length(), system_msg);
}
return system_text;
}
std::string Conversation::GetRoleText(const std::string& role, const std::string& content,
const std::optional<std::string>& fn_call_string) const {
std::string role_text = this->role_templates.at(role);
std::string placeholder = PLACEHOLDERS[MessagePlaceholderFromString(role)];
size_t pos = role_text.find(placeholder);
if (pos != std::string::npos) {
role_text.replace(pos, placeholder.length(), content);
}
if (fn_call_string) {
// replace placeholder[FUNCTION] with function_string
// this assumes function calling is used for a single request scenario only
pos = role_text.find(PLACEHOLDERS[MessagePlaceholders::FUNCTION]);
if (pos != std::string::npos) {
role_text.replace(pos, PLACEHOLDERS[MessagePlaceholders::FUNCTION].length(),
fn_call_string.value());
}
}
return role_text;
}
/// Try to detect if function calling is needed, if so, return the function calling string
Result<std::optional<std::string>> TryGetFunctionCallingString(
const Conversation& conv, const ChatCompletionRequest& request) {
using TResult = Result<std::optional<std::string>>;
if (!request.tools.has_value() ||
(request.tool_choice.has_value() && request.tool_choice.value() == "none")) {
return TResult::Ok(std::nullopt);
}
std::vector<ChatTool> tools_ = request.tools.value();
std::string tool_choice_ = request.tool_choice.value();
// TODO: support with tool choice as dict
for (const auto& tool : tools_) {
if (tool.function.name == tool_choice_) {
tvm::ffi::json::Value function_str(tool.function.AsJSON());
return TResult::Ok(tvm::ffi::json::Stringify(function_str));
}
}
if (tool_choice_ != "auto") {
return TResult::Error("Invalid tool_choice value in the request: " + tool_choice_);
}
tvm::ffi::json::Array function_list;
for (const auto& tool : tools_) {
function_list.push_back(tool.function.AsJSON());
}
tvm::ffi::json::Value function_list_json(function_list);
return TResult::Ok(tvm::ffi::json::Stringify(function_list_json));
};
Result<std::vector<Data>> CreatePrompt(const Conversation& conv,
const ChatCompletionRequest& request,
const ModelConfig& config, DLDevice device) {
using TResult = Result<std::vector<Data>>;
Result<std::optional<std::string>> fn_call_str_tmp = TryGetFunctionCallingString(conv, request);
if (fn_call_str_tmp.IsErr()) {
return TResult::Error(fn_call_str_tmp.UnwrapErr());
}
std::optional<std::string> fn_call_string = fn_call_str_tmp.Unwrap();
// Handle system message
// concz
bool has_custom_system = false;
std::string custom_system_inputs;
auto f_populate_system_message = [&](const std::vector<ChatCompletionMessage>& msg_vec) {
for (ChatCompletionMessage msg : msg_vec) {
if (msg.role == "system") {
TVM_FFI_ICHECK(msg.content.IsText()) << "System message must be text";
custom_system_inputs += msg.content.Text();
has_custom_system = true;
}
}
};
// go through messages in template and passed in.
f_populate_system_message(conv.messages);
f_populate_system_message(request.messages);
// pending text records the text to be put into data
// we lazily accumulate the pending text
// to reduce amount of segments in the Data vector
std::string pending_text =
conv.GetSystemText(has_custom_system ? custom_system_inputs : conv.system_message);
// Get the message strings
std::vector<Data> message_list;
size_t non_system_msg_count = 0;
// returns error if error happens
auto f_process_messages =
[&](const std::vector<ChatCompletionMessage>& msg_vec) -> std::optional<TResult> {
for (size_t i = 0; i < msg_vec.size(); ++i) {
const ChatCompletionMessage& msg = msg_vec[i];
// skip system message as it is already processed
if (msg.role == "system") continue;
auto role_it = conv.roles.find(msg.role);
if (role_it == conv.roles.end()) {
return TResult::Error("Role \"" + msg.role + "\" is not supported");
}
const std::string& role_name = role_it->second;
// skip when content is empty
if (msg.content.IsNull()) {
pending_text += role_name + conv.role_empty_sep;
continue;
}
++non_system_msg_count;
// assistant uses conv.seps[1] if there are two seps
int sep_offset = msg.role == "assistant" ? 1 : 0;
const std::string& seperator = conv.seps[sep_offset % conv.seps.size()];
// setup role prefix
std::string role_prefix = "";
// Do not append role prefix if this is the first message and there is already a system
// message
if (conv.add_role_after_system_message || pending_text.empty() || non_system_msg_count != 1) {
role_prefix = role_name + conv.role_content_sep;
}
pending_text += role_prefix;
if (msg.content.IsParts()) {
for (const auto& item : msg.content.Parts()) {
auto it_type = item.find("type");
if (it_type == item.end()) {
return TResult::Error("The content of a message does not have \"type\" field");
}
if (it_type->second == "text") {
auto it_text = item.find("text");
if (it_text == item.end()) {
return TResult::Error(
"The text type content of a message does not have \"text\" field");
}
// replace placeholder[ROLE] with input message from role
pending_text += conv.GetRoleText(msg.role, it_text->second, fn_call_string);
} else if (it_type->second == "image_url") {
if (item.find("image_url") == item.end()) {
return TResult::Error("Content should have an image_url field");
}
std::string image_url =
item.at("image_url"); // TODO(mlc-team): According to OpenAI API reference this
// should be a map, with a "url" key containing the URL, but
// we are just assuming this as the URL for now
std::string base64_image = image_url.substr(image_url.find(",") + 1);
Result<Tensor> image_data_res = LoadImageFromBase64(base64_image);
if (image_data_res.IsErr()) {
return TResult::Error(image_data_res.UnwrapErr());
}
if (!config.vision_config.has_value()) {
return TResult::Error("Vision config is required for image input");
}
int image_size = config.vision_config.value().image_size;
int patch_size = config.vision_config.value().patch_size;
int embed_size = (image_size * image_size) / (patch_size * patch_size);
Tensor image_data = image_data_res.Unwrap();
std::vector<int64_t> new_shape = {1, image_size, image_size, 3};
Tensor image_tensor = image_data.CreateView(new_shape, image_data.DataType());
// TODO: Not sure if commenting will affect other functions. But
// python part will do clip preprocessing. auto image_tensor =
// ClipPreprocessor(image_data_res.Unwrap(), image_size, device);
// lazily commit text data
if (pending_text.length() != 0) {
message_list.push_back(TextData(pending_text));
pending_text = "";
}
message_list.push_back(ImageData(image_tensor, embed_size));
} else {
return TResult::Error("Unsupported content type: " + it_type->second);
}
}
} else {
TVM_FFI_ICHECK(msg.content.IsText());
pending_text += conv.GetRoleText(msg.role, msg.content.Text(), fn_call_string);
}
pending_text += seperator;
}
return std::nullopt;
};
// Optionally strip `<think>...</think>` blocks from historical assistant
// messages (those before the last user message), mirroring Qwen3's HF chat
// template. See mlc-ai/mlc-llm#3482.
const std::vector<ChatCompletionMessage>* conv_messages_ptr = &conv.messages;
const std::vector<ChatCompletionMessage>* request_messages_ptr = &request.messages;
std::vector<ChatCompletionMessage> stripped_conv_messages;
std::vector<ChatCompletionMessage> stripped_request_messages;
if (conv.strip_reasoning_in_history) {
const size_t conv_size = conv.messages.size();
int64_t last_user_idx = -1;
for (size_t i = 0; i < conv_size; ++i) {
if (conv.messages[i].role == "user") last_user_idx = static_cast<int64_t>(i);
}
for (size_t i = 0; i < request.messages.size(); ++i) {
if (request.messages[i].role == "user") last_user_idx = static_cast<int64_t>(conv_size + i);
}
const std::string kCloseTag = "</think>";
auto strip_range = [&](const std::vector<ChatCompletionMessage>& in, size_t offset,
std::vector<ChatCompletionMessage>& out) {
out.reserve(in.size());
for (size_t i = 0; i < in.size(); ++i) {
ChatCompletionMessage msg = in[i];
const int64_t global_idx = static_cast<int64_t>(offset + i);
if (msg.role == "assistant" && global_idx < last_user_idx && msg.content.IsText()) {
const std::string& text = msg.content.Text();
const size_t close_pos = text.rfind(kCloseTag);
if (close_pos != std::string::npos) {
size_t start = close_pos + kCloseTag.size();
while (start < text.size() && text[start] == '\n') ++start;
msg.content = ChatCompletionMessageContent(text.substr(start));
}
}
out.push_back(std::move(msg));
}
};
strip_range(conv.messages, 0, stripped_conv_messages);
strip_range(request.messages, conv_size, stripped_request_messages);
conv_messages_ptr = &stripped_conv_messages;
request_messages_ptr = &stripped_request_messages;
}
if (auto err = f_process_messages(*conv_messages_ptr)) {
return err.value();
}
if (auto err = f_process_messages(*request_messages_ptr)) {
return err.value();
}
// append last assistant begin message
ChatCompletionMessage last_assistant_begin;
last_assistant_begin.role = "assistant";
last_assistant_begin.content = std::nullopt;
if (auto err = f_process_messages({last_assistant_begin})) {
return err.value();
}
if (pending_text.length() != 0) {
message_list.push_back(TextData(pending_text));
}
// Handle system_prefix_token_ids
if (conv.system_prefix_token_ids.has_value()) {
message_list.insert(message_list.begin(), TokenData(conv.system_prefix_token_ids.value()));
}
return TResult::Ok(message_list);
}
Result<Conversation> Conversation::FromJSON(const tvm::ffi::json::Object& json_obj) {
using TResult = Result<Conversation>;
Conversation conv;
Result<std::optional<std::string>> name_res =
json::LookupOptionalWithResultReturn<std::string>(json_obj, "name");
if (name_res.IsErr()) {
return TResult::Error(name_res.UnwrapErr());
}
conv.name = name_res.Unwrap();
Result<std::string> system_template_res =
json::LookupWithResultReturn<std::string>(json_obj, "system_template");
if (system_template_res.IsErr()) {
return TResult::Error(system_template_res.UnwrapErr());
}
conv.system_template = system_template_res.Unwrap();
Result<std::string> system_message_res =
json::LookupWithResultReturn<std::string>(json_obj, "system_message");
if (system_message_res.IsErr()) {
return TResult::Error(system_message_res.UnwrapErr());
}
conv.system_message = system_message_res.Unwrap();
Result<std::optional<tvm::ffi::json::Array>> system_prefix_token_ids_arr_res =
json::LookupOptionalWithResultReturn<tvm::ffi::json::Array>(json_obj,
"system_prefix_token_ids");
if (system_prefix_token_ids_arr_res.IsErr()) {
return TResult::Error(system_prefix_token_ids_arr_res.UnwrapErr());
}
std::optional<tvm::ffi::json::Array> system_prefix_token_ids_arr =
system_prefix_token_ids_arr_res.Unwrap();
if (system_prefix_token_ids_arr.has_value()) {
std::vector<int> system_prefix_token_ids;
system_prefix_token_ids.reserve(system_prefix_token_ids_arr.value().size());
for (const auto& token_id : system_prefix_token_ids_arr.value()) {
if (!token_id.try_cast<int64_t>().has_value()) {
return TResult::Error("A system prefix token id is not integer.");
}
system_prefix_token_ids.push_back(static_cast<int>(token_id.cast<int64_t>()));
}
conv.system_prefix_token_ids = std::move(system_prefix_token_ids);
}
Result<bool> add_role_after_system_message_res =
json::LookupWithResultReturn<bool>(json_obj, "add_role_after_system_message");
if (add_role_after_system_message_res.IsErr()) {
return TResult::Error(add_role_after_system_message_res.UnwrapErr());
}
conv.add_role_after_system_message = add_role_after_system_message_res.Unwrap();
Result<tvm::ffi::json::Object> roles_object_res =
json::LookupWithResultReturn<tvm::ffi::json::Object>(json_obj, "roles");
if (roles_object_res.IsErr()) {
return TResult::Error(roles_object_res.UnwrapErr());
}
for (const auto& role : roles_object_res.Unwrap()) {
if (!role.second.try_cast<std::string>().has_value()) {
return TResult::Error("A role value in the conversation template is not a string.");
}
conv.roles[role.first.cast<tvm::ffi::String>()] = role.second.cast<std::string>();
}
Result<std::optional<tvm::ffi::json::Object>> role_templates_object_res =
json::LookupOptionalWithResultReturn<tvm::ffi::json::Object>(json_obj, "role_templates");
if (role_templates_object_res.IsErr()) {
return TResult::Error(role_templates_object_res.UnwrapErr());
}
std::optional<tvm::ffi::json::Object> role_templates_object = role_templates_object_res.Unwrap();
if (role_templates_object.has_value()) {
for (const auto& [role, msg] : role_templates_object.value()) {
if (!msg.try_cast<std::string>().has_value()) {
return TResult::Error("A value in \"role_templates\" is not a string.");
}
conv.role_templates[role.cast<tvm::ffi::String>()] = msg.cast<std::string>();
}
}
Result<tvm::ffi::json::Array> messages_arr_res =
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "messages");
if (messages_arr_res.IsErr()) {
return TResult::Error(messages_arr_res.UnwrapErr());
}
for (const auto& message : messages_arr_res.Unwrap()) {
if (!message.try_cast<tvm::ffi::json::Array>().has_value() ||
message.cast<tvm::ffi::json::Array>().size() != 2) {
return TResult::Error(
"A message in the conversation template is not an array of [role, content].");
}
tvm::ffi::json::Array message_arr = message.cast<tvm::ffi::json::Array>();
if (!message_arr[0].try_cast<std::string>().has_value()) {
return TResult::Error("The role of a message in the conversation template is not a string.");
}
std::string role = message_arr[0].cast<std::string>();
// content can be a string or an array of objects
if (message_arr[1].try_cast<std::string>().has_value()) {
ChatCompletionMessage msg;
msg.role = role;
msg.content = message_arr[1].cast<std::string>();
conv.messages.push_back(msg);
continue;
} else if (message_arr[1].try_cast<tvm::ffi::json::Array>().has_value()) {
tvm::ffi::json::Array content_arr = message_arr[1].cast<tvm::ffi::json::Array>();
std::vector<std::unordered_map<std::string, std::string>> content;
content.reserve(content_arr.size());
for (const auto& item : content_arr) {
if (!item.try_cast<tvm::ffi::json::Object>().has_value()) {
return TResult::Error("The content of conversation template message is not an object");
}
std::unordered_map<std::string, std::string> item_map;
for (const auto& [key, value] : item.cast<tvm::ffi::json::Object>()) {
item_map[key.cast<tvm::ffi::String>()] = tvm::ffi::json::Stringify(value);
}
content.push_back(std::move(item_map));
}
ChatCompletionMessage msg;
msg.role = role;
msg.content = content;
conv.messages.push_back(msg);
continue;
} else {
return TResult::Error(
"The content of a message in the conversation template is not a string or an array.");
}
}
Result<tvm::ffi::json::Array> seps_arr_res =
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "seps");
if (seps_arr_res.IsErr()) {
return TResult::Error(seps_arr_res.UnwrapErr());
}
std::vector<std::string> seps;
for (const auto& sep : seps_arr_res.Unwrap()) {
if (!sep.try_cast<std::string>().has_value()) {
return TResult::Error("A separator (\"seps\") of the conversation template is not a string");
}
conv.seps.push_back(sep.cast<std::string>());
}
Result<std::string> role_content_sep_res =
json::LookupWithResultReturn<std::string>(json_obj, "role_content_sep");
if (role_content_sep_res.IsErr()) {
return TResult::Error(role_content_sep_res.UnwrapErr());
}
conv.role_content_sep = role_content_sep_res.Unwrap();
Result<std::string> role_empty_sep_res =
json::LookupWithResultReturn<std::string>(json_obj, "role_empty_sep");
if (role_empty_sep_res.IsErr()) {
return TResult::Error(role_empty_sep_res.UnwrapErr());
}
conv.role_empty_sep = role_empty_sep_res.Unwrap();
Result<tvm::ffi::json::Array> stop_str_arr_res =
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "stop_str");
if (stop_str_arr_res.IsErr()) {
return TResult::Error(stop_str_arr_res.UnwrapErr());
}
for (const auto& stop : stop_str_arr_res.Unwrap()) {
if (!stop.try_cast<std::string>().has_value()) {
return TResult::Error(
"A stop string (\"stop_str\") of the conversation template is not a string.");
}
conv.stop_str.push_back(stop.cast<std::string>());
}
Result<tvm::ffi::json::Array> stop_token_ids_arr_res =
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "stop_token_ids");
if (stop_token_ids_arr_res.IsErr()) {
return TResult::Error(stop_token_ids_arr_res.UnwrapErr());
}
for (const auto& stop : stop_token_ids_arr_res.Unwrap()) {
if (!stop.try_cast<int64_t>().has_value()) {
return TResult::Error(
"A stop token id (\"stop_token_ids\") of the conversation template is not an integer.");
}
conv.stop_token_ids.push_back(static_cast<int>(stop.cast<int64_t>()));
}
Result<std::optional<bool>> strip_reasoning_res =
json::LookupOptionalWithResultReturn<bool>(json_obj, "strip_reasoning_in_history");
if (strip_reasoning_res.IsErr()) {
return TResult::Error(strip_reasoning_res.UnwrapErr());
}
conv.strip_reasoning_in_history = strip_reasoning_res.Unwrap().value_or(false);
return TResult::Ok(conv);
}
Result<Conversation> Conversation::FromJSON(const std::string& json_str) {
Result<tvm::ffi::json::Object> json_obj = json::ParseToJSONObjectWithResultReturn(json_str);
if (json_obj.IsErr()) {
return Result<Conversation>::Error(json_obj.UnwrapErr());
}
return Conversation::FromJSON(json_obj.Unwrap());
}
} // namespace json_ffi
} // namespace llm
} // namespace mlc
+161
View File
@@ -0,0 +1,161 @@
#ifndef MLC_LLM_JSON_FFI_CONV_TEMPLATE_H
#define MLC_LLM_JSON_FFI_CONV_TEMPLATE_H
#include <tvm/ffi/extra/json.h>
#include <iostream>
#include <map>
#include <optional>
#include <string>
#include <typeinfo>
#include <variant>
#include <vector>
#include "../serve/data.h"
#include "../support/result.h"
#include "openai_api_protocol.h"
using namespace mlc::llm::serve;
namespace mlc {
namespace llm {
namespace json_ffi {
/****************** Model vision config ******************/
/*! \brief Defines the Vision config of the model (if present) */
class ModelVisionConfig {
public:
int hidden_size;
int image_size;
int intermediate_size;
int num_attention_heads;
int num_hidden_layers;
int patch_size;
int projection_dim;
int vocab_size;
std::string dtype;
int num_channels;
double layer_norm_eps;
static ModelVisionConfig FromJSON(const tvm::ffi::json::Object& json_obj);
};
/****************** Model config ******************/
/*! \brief Defines the config of the model.
Populated from "model_config" field in mlc-chat-config.json */
class ModelConfig {
public:
int vocab_size;
int context_window_size;
int sliding_window_size;
int prefill_chunk_size;
int tensor_parallel_shards;
int pipeline_parallel_stages;
int max_batch_size;
std::optional<ModelVisionConfig> vision_config = std::nullopt;
static ModelConfig FromJSON(const tvm::ffi::json::Object& json_obj);
};
/****************** Conversation template ******************/
enum class MessagePlaceholders { SYSTEM, USER, ASSISTANT, TOOL, FUNCTION };
MessagePlaceholders MessagePlaceholderFromString(const std::string& role);
/**
* @brief A struct that specifies the convention template of conversation
* and contains the conversation history.
*/
struct Conversation {
// Optional name of the template.
std::optional<std::string> name = std::nullopt;
// The system prompt template, it optionally contains the system
// message placeholder, and the placeholder will be replaced with
// the system message below.
std::string system_template;
// The content of the system prompt (without the template format).
std::string system_message;
// The system token ids to be prepended at the beginning of tokenized
// generated prompt.
std::optional<std::vector<int>> system_prefix_token_ids = std::nullopt;
// Whether or not to append user role and separator after the system message.
// This is mainly for [INST] [/INST] style prompt format
bool add_role_after_system_message = true;
// The conversation roles
std::unordered_map<std::string, std::string> roles;
// The roles prompt template, it optionally contains the defaults
// message placeholders and will be replaced by actual content
std::unordered_map<std::string, std::string> role_templates;
// The conversation history messages.
// Each message is a pair of strings, denoting "(role, content)".
// The content can be None.
std::vector<ChatCompletionMessage> messages;
// The separators between messages when concatenating into a single prompt.
// List size should be either 1 or 2.
// - When size is 1, the separator will be used between adjacent messages.
// - When size is 2, seps[0] is used after user message, and
// seps[1] is used after assistant message.
std::vector<std::string> seps;
// The separator between the role and the content in a message.
std::string role_content_sep;
// The separator between the role and empty contents.
std::string role_empty_sep;
// The stop criteria
std::vector<std::string> stop_str;
std::vector<int> stop_token_ids;
// When true, strip `<think>...</think>` blocks (and any trailing whitespace)
// from historical assistant messages before rendering the prompt, mirroring
// Qwen3's official HF chat template. Only assistant messages that appear
// before the last user message are affected.
bool strip_reasoning_in_history = false;
Conversation();
/*!
* \brief Get the system text(with the prompt template) given the system prompt message
* \param system_msg The system prompt message.
* \return The created system text.
*/
std::string GetSystemText(const std::string& system_msg) const;
/*!
* \brief replace the content from role by the correct role text in template
* \param role The input role
* \param content The input content from the role
* \param fn_call_str The function calling string if any.
* \return The created text.
*/
std::string GetRoleText(const std::string& role, const std::string& content,
const std::optional<std::string>& fn_call_str) const;
/*! \brief Create a Conversation instance from the given JSON object. */
static Result<Conversation> FromJSON(const tvm::ffi::json::Object& json);
/*! \brief Parse and create a Conversation instance from the given JSON string. */
static Result<Conversation> FromJSON(const std::string& json_str);
};
/*! \brief Create the list of prompts from the messages based on the conversation template. */
Result<std::vector<Data>> CreatePrompt(const Conversation& conv,
const ChatCompletionRequest& request,
const ModelConfig& config, DLDevice device);
} // namespace json_ffi
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_JSON_FFI_CONV_TEMPLATE_H
+160
View File
@@ -0,0 +1,160 @@
#include "image_utils.h"
#include <tvm/support/io.h>
#include "../../3rdparty/tvm/src/support/base64.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
namespace mlc {
namespace llm {
namespace json_ffi {
using namespace tvm::runtime;
class MemoryBufferStream : public tvm::support::Stream {
public:
using Stream::Read;
using Stream::Write;
MemoryBufferStream(const char* data, size_t size) : data_(data), size_(size), pos_(0) {}
size_t Read(void* ptr, size_t size) override {
size_t remaining = size_ - pos_;
if (size > remaining) {
size = remaining;
}
if (size == 0) {
return 0;
}
std::memcpy(ptr, data_ + pos_, size);
pos_ += size;
return size;
}
size_t Write(const void* ptr, size_t size) override {
TVM_FFI_THROW(InternalError) << "MemoryBufferStream does not support write";
return 0;
}
private:
const char* data_;
size_t size_;
size_t pos_;
};
size_t Base64DecodedSize(const std::string& base64_str) {
size_t len = base64_str.size();
size_t padding = 0;
if (base64_str[len - 1] == '=') {
padding++;
}
if (base64_str[len - 2] == '=') {
padding++;
}
return 3 * len / 4 - padding;
}
Result<Tensor> LoadImageFromBase64(const std::string& base64_str) {
using TResult = Result<Tensor>;
MemoryBufferStream stream(base64_str.c_str(), base64_str.size());
tvm::support::Base64InStream base64_stream(&stream);
size_t decoded_size = Base64DecodedSize(base64_str);
std::vector<unsigned char> decoded(decoded_size);
base64_stream.InitPosition();
base64_stream.Read((void*)decoded.data(), decoded_size);
int width, height, num_channels;
unsigned char* image_data =
stbi_load_from_memory(decoded.data(), decoded_size, &width, &height, &num_channels, 3);
if (!image_data) {
return TResult::Error(stbi_failure_reason());
}
auto image_tensor = Tensor::Empty({height, width, 3}, {kDLUInt, 8, 1}, {kDLCPU, 0});
image_tensor.CopyFromBytes((void*)image_data, width * height * 3);
stbi_image_free(image_data);
return TResult::Ok(image_tensor);
}
Tensor ClipPreprocessor(Tensor image_data, int target_size, DLDevice device) {
int height = image_data->shape[0];
int width = image_data->shape[1];
// Resize
const int short_side = width < height ? width : height;
const int long_side = width > height ? width : height;
const int new_short_side = target_size;
const int new_long_side = (int)(new_short_side * (long_side / (float)short_side));
const int new_width = width < height ? new_short_side : new_long_side;
const int new_height = width > height ? new_short_side : new_long_side;
std::vector<float> processed_image_data(new_width * new_height * 3);
// Bilinear Interpolation
for (int y = 0; y < new_height; y++) {
for (int x = 0; x < new_width; x++) {
const float x_ratio = float(width - 1) / new_width;
const float y_ratio = float(height - 1) / new_height;
const int x1 = int(x_ratio * x);
const int y1 = int(y_ratio * y);
const int x2 = x1 + 1;
const int y2 = y1 + 1;
const float x_diff = x_ratio * x - x1;
const float y_diff = y_ratio * y - y1;
for (int c = 0; c < 3; c++) {
const uint8_t top_left = ((uint8_t*)image_data->data)[(y1 * width + x1) * 3 + c];
const uint8_t top_right = ((uint8_t*)image_data->data)[(y1 * width + x2) * 3 + c];
const uint8_t bottom_left = ((uint8_t*)image_data->data)[(y2 * width + x1) * 3 + c];
const uint8_t bottom_right = ((uint8_t*)image_data->data)[(y2 * width + x2) * 3 + c];
processed_image_data[(y * new_width + x) * 3 + c] =
(float)(int(top_left * (1 - x_diff) * (1 - y_diff) + top_right * x_diff * (1 - y_diff) +
bottom_left * y_diff * (1 - x_diff) + bottom_right * x_diff * y_diff));
}
}
}
// Center crop
const int crop_x = (new_width - target_size) / 2;
const int crop_y = (new_height - target_size) / 2;
std::vector<float> cropped_image_data(target_size * target_size * 3);
for (int y = 0; y < target_size; y++) {
for (int x = 0; x < target_size; x++) {
for (int c = 0; c < 3; c++) {
cropped_image_data[(y * target_size + x) * 3 + c] =
processed_image_data[((y + crop_y) * new_width + x + crop_x) * 3 + c];
}
}
}
// Rescale
for (int i = 0; i < target_size * target_size * 3; i++) {
cropped_image_data[i] = cropped_image_data[i] / 255.0f;
}
// Normalize
const float IMAGE_MEAN[] = {0.48145466f, 0.4578275f, 0.40821073f};
const float IMAGE_STD[] = {0.26862954f, 0.26130258f, 0.27577711f};
for (int i = 0; i < target_size * target_size * 3; i++) {
const int c = i % 3;
cropped_image_data[i] = (cropped_image_data[i] - IMAGE_MEAN[c]) / IMAGE_STD[c];
}
std::vector<float> image_data_channel_first(target_size * target_size * 3);
for (int y = 0; y < target_size; y++) {
for (int x = 0; x < target_size; x++) {
for (int c = 0; c < 3; c++) {
image_data_channel_first[c * target_size * target_size + y * target_size + x] =
cropped_image_data[(y * target_size + x) * 3 + c];
}
}
}
// Create Tensor
auto image_tensor = Tensor::Empty({1, 3, target_size, target_size}, {kDLFloat, 32, 1}, device);
image_tensor.CopyFromBytes((void*)image_data_channel_first.data(),
target_size * target_size * 3 * sizeof(float));
return image_tensor;
}
} // namespace json_ffi
} // namespace llm
} // namespace mlc
+31
View File
@@ -0,0 +1,31 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file json_ffi/image_utils.h
* \brief The header of Image utils for JSON FFI Engine in MLC LLM.
*/
#ifndef MLC_LLM_JSON_FFI_IMAGE_UTILS_H_
#define MLC_LLM_JSON_FFI_IMAGE_UTILS_H_
#include <tvm/runtime/tensor.h>
#include <optional>
#include <string>
#include "../support/result.h"
namespace mlc {
namespace llm {
namespace json_ffi {
/*! \brief Load a base64 encoded image string into a CPU Tensor of shape {height, width, 3} */
Result<tvm::runtime::Tensor> LoadImageFromBase64(const std::string& base64_str);
/*! \brief Preprocess the CPU image for CLIP encoder and return an Tensor on the given device */
tvm::runtime::Tensor ClipPreprocessor(tvm::runtime::Tensor image_data, int target_size,
DLDevice device);
} // namespace json_ffi
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_JSON_FFI_IMAGE_UTILS_H_
+310
View File
@@ -0,0 +1,310 @@
#include "json_ffi_engine.h"
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/extra/module.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include <filesystem>
#include <fstream>
#include "../serve/model.h"
#include "../support/json_parser.h"
#include "../support/module_vtable.h"
#include "../support/result.h"
namespace mlc {
namespace llm {
namespace json_ffi {
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::Shape;
JSONFFIEngine::JSONFFIEngine() { engine_ = serve::ThreadedEngine::Create(); }
bool JSONFFIEngine::ChatCompletion(std::string request_json_str, std::string request_id) {
bool success = this->AddRequest(request_json_str, request_id);
if (!success) {
this->StreamBackError(request_id);
}
return success;
}
void JSONFFIEngine::StreamBackError(std::string request_id) {
ChatCompletionMessage delta;
delta.content = this->err_;
delta.role = "assistant";
ChatCompletionStreamResponseChoice choice;
choice.finish_reason = FinishReason::error;
choice.index = 0;
choice.delta = delta;
ChatCompletionStreamResponse response;
response.id = request_id;
response.choices = std::vector<ChatCompletionStreamResponseChoice>{choice};
response.model = "json_ffi"; // TODO: Return model name from engine (or from args)
response.system_fingerprint = "";
tvm::ffi::json::Array response_arr;
response_arr.push_back(response.AsJSON());
// now stream back the final usage block, which is required.
// NOTE: always stream back final usage block as it is an
// invariant of the system
response.choices.clear();
tvm::ffi::json::Object dummy_usage;
dummy_usage.Set("prompt_tokens", static_cast<int64_t>(0));
dummy_usage.Set("completion_tokens", static_cast<int64_t>(0));
dummy_usage.Set("total_tokens", static_cast<int64_t>(0));
response.usage = tvm::ffi::json::Value(dummy_usage);
response_arr.push_back(response.AsJSON());
std::string stream_back_json = tvm::ffi::json::Stringify(response_arr);
this->request_stream_callback_(stream_back_json);
}
bool JSONFFIEngine::AddRequest(std::string request_json_str, std::string request_id) {
Result<ChatCompletionRequest> request_res = ChatCompletionRequest::FromJSON(request_json_str);
if (request_res.IsErr()) {
err_ = request_res.UnwrapErr();
return false;
}
ChatCompletionRequest request = request_res.Unwrap();
Array<Data> inputs;
Array<String> stop_strs;
bool is_special_request =
(request.debug_config.has_value() &&
request.debug_config.value().special_request != SpecialRequestKind::kNone);
// special request does not have to go through prompt construction
if (!is_special_request) {
// get prompt: note, assistant was appended in the end.
Result<std::vector<Data>> inputs_obj =
CreatePrompt(this->conv_template_, request, this->model_config_, this->device_);
if (inputs_obj.IsErr()) {
err_ = inputs_obj.UnwrapErr();
return false;
}
inputs = inputs_obj.Unwrap();
stop_strs.reserve(this->conv_template_.stop_str.size());
for (const std::string& stop_str : this->conv_template_.stop_str) {
stop_strs.push_back(stop_str);
}
if (request.stop.has_value()) {
stop_strs.reserve(stop_strs.size() + request.stop.value().size());
for (const std::string& stop_str : request.stop.value()) {
stop_strs.push_back(stop_str);
}
}
}
// create a generation config from request
const auto& default_gen_cfg = default_generation_config_;
auto gen_cfg = tvm::ffi::make_object<GenerationConfigNode>();
gen_cfg->n = request.n;
gen_cfg->temperature = request.temperature.value_or(default_gen_cfg->temperature);
gen_cfg->top_p = request.top_p.value_or(default_gen_cfg->top_p);
gen_cfg->frequency_penalty =
request.frequency_penalty.value_or(default_gen_cfg->frequency_penalty);
gen_cfg->presence_penalty = request.presence_penalty.value_or(default_gen_cfg->presence_penalty);
gen_cfg->logprobs = request.logprobs;
gen_cfg->top_logprobs = request.top_logprobs;
gen_cfg->logit_bias = request.logit_bias.value_or(default_gen_cfg->logit_bias);
gen_cfg->seed = request.seed.value_or(std::random_device{}());
gen_cfg->max_tokens = request.max_tokens.value_or(default_gen_cfg->max_tokens);
gen_cfg->stop_strs = std::move(stop_strs);
gen_cfg->stop_token_ids = conv_template_.stop_token_ids;
gen_cfg->response_format = request.response_format.value_or(ResponseFormat());
gen_cfg->debug_config = request.debug_config.value_or(DebugConfig());
Result<GenerationConfig> res_gen_config = GenerationConfig::Validate(GenerationConfig(gen_cfg));
if (res_gen_config.IsErr()) {
err_ = res_gen_config.UnwrapErr();
return false;
}
Request engine_request(request_id, inputs, res_gen_config.Unwrap());
// setup request state
RequestState rstate;
rstate.model = request.model.value_or("");
rstate.streamer.reserve(gen_cfg->n);
for (int i = 0; i < gen_cfg->n; ++i) {
rstate.streamer.push_back(TextStreamer(tokenizer_));
}
request_map_[request_id] = std::move(rstate);
this->engine_->AddRequest(engine_request);
return true;
}
bool JSONFFIEngine::Abort(std::string request_id) {
this->engine_->AbortRequest(request_id);
auto it = request_map_.find(request_id);
if (it != request_map_.end()) {
request_map_.erase(it);
}
return true;
}
std::string JSONFFIEngine::GetLastError() { return err_; }
void JSONFFIEngine::ExitBackgroundLoop() { this->engine_->ExitBackgroundLoop(); }
JSONFFIEngine::~JSONFFIEngine() { this->ExitBackgroundLoop(); }
class JSONFFIEngineImpl : public JSONFFIEngine, public ffi::ModuleObj {
public:
TVM_MODULE_VTABLE_BEGIN("mlc.json_ffi");
TVM_MODULE_VTABLE_ENTRY("init_background_engine", &JSONFFIEngineImpl::InitBackgroundEngine);
TVM_MODULE_VTABLE_ENTRY("reload", &JSONFFIEngineImpl::Reload);
TVM_MODULE_VTABLE_ENTRY("unload", &JSONFFIEngineImpl::Unload);
TVM_MODULE_VTABLE_ENTRY("reset", &JSONFFIEngineImpl::Reset);
TVM_MODULE_VTABLE_ENTRY("chat_completion", &JSONFFIEngineImpl::ChatCompletion);
TVM_MODULE_VTABLE_ENTRY("abort", &JSONFFIEngineImpl::Abort);
TVM_MODULE_VTABLE_ENTRY("get_last_error", &JSONFFIEngineImpl::GetLastError);
TVM_MODULE_VTABLE_ENTRY("run_background_loop", &JSONFFIEngineImpl::RunBackgroundLoop);
TVM_MODULE_VTABLE_ENTRY("run_background_stream_back_loop",
&JSONFFIEngineImpl::RunBackgroundStreamBackLoop);
TVM_MODULE_VTABLE_ENTRY("exit_background_loop", &JSONFFIEngineImpl::ExitBackgroundLoop);
TVM_MODULE_VTABLE_END();
void InitBackgroundEngine(int device_type, int device_id,
Optional<Function> request_stream_callback) {
DLDevice device{static_cast<DLDeviceType>(device_type), device_id};
this->device_ = device;
TVM_FFI_ICHECK(request_stream_callback.has_value())
<< "JSONFFIEngine requires request stream callback function, but it is not given.";
this->request_stream_callback_ = request_stream_callback.value();
auto frequest_stream_callback_wrapper = [this](ffi::PackedArgs args, ffi::Any* ret) {
TVM_FFI_ICHECK_EQ(args.size(), 1);
Array<RequestStreamOutput> delta_outputs = args[0].cast<Array<RequestStreamOutput>>();
std::string responses = this->GetResponseFromStreamOutput(delta_outputs);
this->request_stream_callback_(responses);
};
request_stream_callback = Function(frequest_stream_callback_wrapper);
this->engine_->InitThreadedEngine(device, std::move(request_stream_callback), std::nullopt);
}
void Reload(String engine_config_json_str) {
this->engine_->Reload(engine_config_json_str);
this->default_generation_config_ = this->engine_->GetDefaultGenerationConfig();
auto engine_config = this->engine_->GetCompleteEngineConfig();
// Load conversation template.
Result<tvm::ffi::json::Object> model_config_json =
serve::Model::LoadModelConfig(engine_config->model);
TVM_FFI_ICHECK(model_config_json.IsOk()) << model_config_json.UnwrapErr();
const tvm::ffi::json::Object& model_config_json_unwrapped = model_config_json.Unwrap();
Result<Conversation> conv_template = Conversation::FromJSON(
json::Lookup<tvm::ffi::json::Object>(model_config_json_unwrapped, "conv_template"));
TVM_FFI_ICHECK(!conv_template.IsErr())
<< "Invalid conversation template JSON: " << conv_template.UnwrapErr();
this->conv_template_ = conv_template.Unwrap();
this->model_config_ = ModelConfig::FromJSON(
json::Lookup<tvm::ffi::json::Object>(model_config_json_unwrapped, "model_config"));
this->tokenizer_ = Tokenizer::FromPath(engine_config->model);
}
void Unload() { this->engine_->Unload(); }
void Reset() { this->engine_->Reset(); }
void RunBackgroundLoop() { this->engine_->RunBackgroundLoop(); }
void RunBackgroundStreamBackLoop() { this->engine_->RunBackgroundStreamBackLoop(); }
String GetResponseFromStreamOutput(Array<RequestStreamOutput> delta_outputs) {
tvm::ffi::json::Array json_response_arr;
for (const auto& delta_output : delta_outputs) {
std::string request_id = delta_output->request_id;
auto request_state_it = request_map_.find(request_id);
if (request_state_it == request_map_.end()) continue;
RequestState& rstate = request_state_it->second;
// build the final usage messages
// invariant, we can always let other messages to come first
// then the final usage messages, as final usage is always last
if (delta_output->request_final_usage_json_str.has_value()) {
ChatCompletionStreamResponse response;
response.id = request_id;
response.model = rstate.model;
response.system_fingerprint = "";
std::string usage_json_str = delta_output->request_final_usage_json_str.value();
tvm::ffi::String parse_err;
auto usage_json = tvm::ffi::json::Parse(usage_json_str, &parse_err);
if (!parse_err.empty()) {
err_ = parse_err;
} else {
response.usage = usage_json;
}
json_response_arr.push_back(response.AsJSON());
request_map_.erase(request_state_it);
continue;
}
TVM_FFI_ICHECK_NE(delta_output->group_finish_reason.size(), 0);
TVM_FFI_ICHECK_EQ(delta_output->group_delta_token_ids.size(),
delta_output->group_finish_reason.size());
TVM_FFI_ICHECK_EQ(delta_output->group_delta_token_ids.size(), rstate.streamer.size());
ChatCompletionStreamResponse response;
response.id = request_id;
response.model = rstate.model;
response.system_fingerprint = "";
for (size_t i = 0; i < delta_output->group_finish_reason.size(); ++i) {
// choice
ChatCompletionStreamResponseChoice choice;
Optional<String> finish_reason = delta_output->group_finish_reason[i];
if (finish_reason.has_value()) {
if (finish_reason.value() == "stop") {
choice.finish_reason = FinishReason::stop;
} else if (finish_reason.value() == "length") {
choice.finish_reason = FinishReason::length;
} else if (finish_reason.value() == "tool_calls") {
choice.finish_reason = FinishReason::tool_calls;
} else if (finish_reason.value() == "error") {
choice.finish_reason = FinishReason::error;
}
} else {
choice.finish_reason = std::nullopt;
}
choice.index = static_cast<int>(i);
ChatCompletionMessage delta;
// Size of delta_output->group_delta_token_ids Array should be 1
const Shape& delta_token_ids = delta_output->group_delta_token_ids[i];
std::vector<int32_t> delta_token_ids_vec(delta_token_ids.begin(), delta_token_ids.end());
std::string content = rstate.streamer[i]->Put(delta_token_ids_vec);
if (finish_reason.has_value()) {
content += rstate.streamer[i]->Finish();
}
if (!content.empty()) {
delta.content = content;
}
delta.role = "assistant";
choice.delta = delta;
if (!choice.delta.content.IsNull() || choice.finish_reason.has_value()) {
response.choices.push_back(choice);
}
}
// if it is not the usage block, choices cannot be empty
if (!response.choices.empty()) {
json_response_arr.push_back(response.AsJSON());
}
}
return tvm::ffi::json::Stringify(json_response_arr);
}
};
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("mlc.json_ffi.CreateJSONFFIEngine",
[]() { return ffi::Module(tvm::ffi::make_object<JSONFFIEngineImpl>()); });
}
} // namespace json_ffi
} // namespace llm
} // namespace mlc
+74
View File
@@ -0,0 +1,74 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file json_ffi/json_ffi_engine.h
* \brief The header of JSON FFI engine in MLC LLM.
*/
#ifndef MLC_LLM_JSON_FFI_JSON_FFI_ENGINE_H_
#define MLC_LLM_JSON_FFI_JSON_FFI_ENGINE_H_
#include <string>
#include "../serve/threaded_engine.h"
#include "../tokenizers/streamer.h"
#include "conv_template.h"
#include "openai_api_protocol.h"
namespace mlc {
namespace llm {
namespace json_ffi {
using namespace tvm::runtime;
using namespace mlc::llm::serve;
/*!
* \brief // Todo: document this class, fields and member functions
*/
class JSONFFIEngine {
public:
JSONFFIEngine();
~JSONFFIEngine();
bool ChatCompletion(std::string request_json_str, std::string request_id);
bool AddRequest(std::string request_json_str, std::string request_id);
void StreamBackError(std::string request_id);
bool Abort(std::string request_id);
std::string GetLastError();
void ExitBackgroundLoop();
protected:
/*! \brief local request state entry, one per reply stream. */
struct RequestState {
/*! \brief model to fill in reply. */
std::string model;
/*! \brief text streamer for each stream */
std::vector<TextStreamer> streamer;
};
std::unique_ptr<ThreadedEngine> engine_;
std::string err_;
Function request_stream_callback_;
// tokenizer
Tokenizer tokenizer_;
// conversation template
Conversation conv_template_;
// generation config
GenerationConfig default_generation_config_;
// model config
ModelConfig model_config_;
// local device
DLDevice device_;
// request state map
std::unordered_map<String, RequestState> request_map_;
};
} // namespace json_ffi
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_JSON_FFI_JSON_FFI_ENGINE_H_
+543
View File
@@ -0,0 +1,543 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file json_ffi/openai_api_protocol.cc
* \brief The implementation of OpenAI API Protocol in MLC LLM.
*/
#include "openai_api_protocol.h"
#include "../support/json_parser.h"
namespace mlc {
namespace llm {
namespace json_ffi {
Result<ChatFunction> ChatFunction::FromJSON(const tvm::ffi::json::Object& json_obj) {
using TResult = Result<ChatFunction>;
ChatFunction chat_func;
// description
Result<std::optional<std::string>> description_res =
json::LookupOptionalWithResultReturn<std::string>(json_obj, "description");
if (description_res.IsErr()) {
return TResult::Error(description_res.UnwrapErr());
}
chat_func.description = description_res.Unwrap();
// name
Result<std::string> name_res = json::LookupWithResultReturn<std::string>(json_obj, "name");
if (name_res.IsErr()) {
return TResult::Error(name_res.UnwrapErr());
}
chat_func.name = name_res.Unwrap();
// parameters
Result<tvm::ffi::json::Object> parameters_obj_res =
json::LookupWithResultReturn<tvm::ffi::json::Object>(json_obj, "parameters");
if (parameters_obj_res.IsErr()) {
return TResult::Error(parameters_obj_res.UnwrapErr());
}
tvm::ffi::json::Object parameters_obj = parameters_obj_res.Unwrap();
chat_func.parameters.reserve(parameters_obj.size());
for (const auto& [key, value] : parameters_obj) {
chat_func.parameters[key.cast<tvm::ffi::String>()] = tvm::ffi::json::Stringify(value);
}
return TResult::Ok(chat_func);
}
tvm::ffi::json::Object ChatFunction::AsJSON() const {
tvm::ffi::json::Object obj;
if (this->description.has_value()) {
obj.Set("description", this->description.value());
}
obj.Set("name", this->name);
tvm::ffi::json::Object parameters_obj;
for (const auto& pair : this->parameters) {
parameters_obj.Set(pair.first, pair.second);
}
obj.Set("parameters", parameters_obj);
return obj;
}
Result<ChatTool> ChatTool::FromJSON(const tvm::ffi::json::Object& json_obj) {
using TResult = Result<ChatTool>;
ChatTool chatTool;
// function
Result<tvm::ffi::json::Object> function_obj_res =
json::LookupWithResultReturn<tvm::ffi::json::Object>(json_obj, "function");
if (function_obj_res.IsErr()) {
return TResult::Error(function_obj_res.UnwrapErr());
}
Result<ChatFunction> function = ChatFunction::FromJSON(function_obj_res.Unwrap());
if (function.IsErr()) {
return TResult::Error(function.UnwrapErr());
}
chatTool.function = function.Unwrap();
return TResult::Ok(chatTool);
}
tvm::ffi::json::Object ChatTool::AsJSON() const {
tvm::ffi::json::Object obj;
obj.Set("type", "function");
obj.Set("function", this->function.AsJSON());
return obj;
}
Result<ChatFunctionCall> ChatFunctionCall::FromJSON(const tvm::ffi::json::Object& json_obj) {
using TResult = Result<ChatFunctionCall>;
ChatFunctionCall chat_func_call;
// name
Result<std::string> name_res = json::LookupWithResultReturn<std::string>(json_obj, "name");
if (name_res.IsErr()) {
return TResult::Error(name_res.UnwrapErr());
}
chat_func_call.name = name_res.Unwrap();
// arguments
Result<std::optional<tvm::ffi::json::Object>> arguments_obj_res =
json::LookupOptionalWithResultReturn<tvm::ffi::json::Object>(json_obj, "arguments");
if (arguments_obj_res.IsErr()) {
return TResult::Error(arguments_obj_res.UnwrapErr());
}
std::optional<tvm::ffi::json::Object> arguments_obj = arguments_obj_res.Unwrap();
if (arguments_obj.has_value()) {
std::unordered_map<std::string, std::string> arguments;
arguments.reserve(arguments_obj.value().size());
for (const auto& [key, value] : arguments_obj.value()) {
arguments[key.cast<tvm::ffi::String>()] = tvm::ffi::json::Stringify(value);
}
chat_func_call.arguments = std::move(arguments);
}
return TResult::Ok(chat_func_call);
}
tvm::ffi::json::Object ChatFunctionCall::AsJSON() const {
tvm::ffi::json::Object obj;
tvm::ffi::json::Object arguments_obj;
if (this->arguments.has_value()) {
for (const auto& pair : this->arguments.value()) {
arguments_obj.Set(pair.first, pair.second);
}
obj.Set("arguments", arguments_obj);
}
obj.Set("name", this->name);
return obj;
}
Result<ChatToolCall> ChatToolCall::FromJSON(const tvm::ffi::json::Object& json_obj) {
using TResult = Result<ChatToolCall>;
ChatToolCall chat_tool_call;
// function
Result<tvm::ffi::json::Object> function_obj_res =
json::LookupWithResultReturn<tvm::ffi::json::Object>(json_obj, "function");
if (function_obj_res.IsErr()) {
return TResult::Error(function_obj_res.UnwrapErr());
}
Result<ChatFunctionCall> function_res = ChatFunctionCall::FromJSON(function_obj_res.Unwrap());
if (function_res.IsErr()) {
return TResult::Error(function_res.UnwrapErr());
}
chat_tool_call.function = function_res.Unwrap();
// overwrite default id
Result<std::optional<std::string>> id_res =
json::LookupOptionalWithResultReturn<std::string>(json_obj, "id");
if (id_res.IsErr()) {
return TResult::Error(id_res.UnwrapErr());
}
std::optional<std::string> id = id_res.UnwrapErr();
if (id.has_value()) {
chat_tool_call.id = id.value();
}
return TResult::Ok(chat_tool_call);
}
tvm::ffi::json::Object ChatToolCall::AsJSON() const {
tvm::ffi::json::Object obj;
obj.Set("id", this->id);
obj.Set("function", this->function.AsJSON());
obj.Set("type", "function");
return obj;
}
Result<ChatCompletionMessage> ChatCompletionMessage::FromJSON(
const tvm::ffi::json::Object& json_obj) {
using TResult = Result<ChatCompletionMessage>;
ChatCompletionMessage message;
ChatCompletionMessageContent content;
// content
if (json_obj.count("content") == 0) {
return TResult::Error("ValueError: key \"content\" not found in the chat completion.");
}
tvm::ffi::json::Value content_val = json_obj.at("content");
if (content_val.try_cast<std::string>().has_value()) {
content = content_val.cast<std::string>();
} else if (content_val == nullptr) {
// skip
} else {
// most complicated case
std::vector<std::unordered_map<std::string, std::string>> parts;
Result<tvm::ffi::json::Array> content_arr_res =
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "content");
if (content_arr_res.IsErr()) {
return TResult::Error(content_arr_res.UnwrapErr());
}
tvm::ffi::json::Array content_arr = content_arr_res.Unwrap();
for (const auto& item : content_arr) {
if (!item.try_cast<tvm::ffi::json::Object>().has_value()) {
return TResult::Error("The content of chat completion message is not an object");
}
tvm::ffi::json::Object item_obj = item.cast<tvm::ffi::json::Object>();
std::unordered_map<std::string, std::string> item_map;
for (const auto& [key, value] : item_obj) {
item_map[key.cast<tvm::ffi::String>()] = tvm::ffi::json::Stringify(value);
}
parts.push_back(std::move(item_map));
}
content = parts;
}
message.content = content;
// role
Result<std::string> role_str_res = json::LookupWithResultReturn<std::string>(json_obj, "role");
if (role_str_res.IsErr()) {
return TResult::Error(role_str_res.UnwrapErr());
}
std::string role_str = role_str_res.Unwrap();
if (role_str == "system" || role_str == "user" || role_str == "assistant" || role_str == "tool") {
message.role = role_str;
} else {
return TResult::Error("Invalid role in chat completion message: " + role_str);
}
// name
Result<std::optional<std::string>> name_res =
json::LookupOptionalWithResultReturn<std::string>(json_obj, "name");
if (name_res.IsErr()) {
return TResult::Error(name_res.UnwrapErr());
}
message.name = name_res.Unwrap();
// tool calls
Result<std::optional<tvm::ffi::json::Array>> tool_calls_arr_res =
json::LookupOptionalWithResultReturn<tvm::ffi::json::Array>(json_obj, "tool_calls");
if (tool_calls_arr_res.IsErr()) {
return TResult::Error(tool_calls_arr_res.UnwrapErr());
}
std::optional<tvm::ffi::json::Array> tool_calls_arr = tool_calls_arr_res.Unwrap();
if (tool_calls_arr.has_value()) {
std::vector<ChatToolCall> tool_calls;
tool_calls.reserve(tool_calls_arr.value().size());
for (const auto& item : tool_calls_arr.value()) {
if (!item.try_cast<tvm::ffi::json::Object>().has_value()) {
return TResult::Error("A tool call item in the chat completion message is not an object");
}
Result<ChatToolCall> tool_call = ChatToolCall::FromJSON(item.cast<tvm::ffi::json::Object>());
if (tool_call.IsErr()) {
return TResult::Error(tool_call.UnwrapErr());
}
tool_calls.push_back(tool_call.Unwrap());
}
message.tool_calls = tool_calls;
}
// tool call id
Result<std::optional<std::string>> tool_call_id_res =
json::LookupOptionalWithResultReturn<std::string>(json_obj, "tool_call_id");
if (tool_call_id_res.IsErr()) {
return TResult::Error(tool_call_id_res.UnwrapErr());
}
message.tool_call_id = tool_call_id_res.Unwrap();
return TResult::Ok(message);
}
Result<ChatCompletionRequest> ChatCompletionRequest::FromJSON(const std::string& json_str) {
using TResult = Result<ChatCompletionRequest>;
Result<tvm::ffi::json::Object> json_obj_res = json::ParseToJSONObjectWithResultReturn(json_str);
if (json_obj_res.IsErr()) {
return TResult::Error(json_obj_res.UnwrapErr());
}
tvm::ffi::json::Object json_obj = json_obj_res.Unwrap();
ChatCompletionRequest request;
// messages
Result<tvm::ffi::json::Array> messages_arr_res =
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "messages");
if (messages_arr_res.IsErr()) {
return TResult::Error(messages_arr_res.UnwrapErr());
}
std::vector<ChatCompletionMessage> messages;
tvm::ffi::json::Array messages_arr = messages_arr_res.Unwrap();
for (const auto& item : messages_arr) {
if (!item.try_cast<tvm::ffi::json::Object>().has_value()) {
return TResult::Error("A message in chat completion request is not object");
}
tvm::ffi::json::Object item_obj = item.cast<tvm::ffi::json::Object>();
Result<ChatCompletionMessage> message = ChatCompletionMessage::FromJSON(item_obj);
if (message.IsErr()) {
return TResult::Error(message.UnwrapErr());
}
messages.push_back(message.Unwrap());
}
request.messages = messages;
// model
Result<std::optional<std::string>> model_res =
json::LookupOptionalWithResultReturn<std::string>(json_obj, "model");
if (model_res.IsErr()) {
return TResult::Error(model_res.UnwrapErr());
}
request.model = model_res.Unwrap();
// temperature
Result<std::optional<double>> temperature_res =
json::LookupOptionalWithResultReturn<double>(json_obj, "temperature");
if (temperature_res.IsErr()) {
return TResult::Error(temperature_res.UnwrapErr());
}
request.temperature = temperature_res.Unwrap();
// top_p
Result<std::optional<double>> top_p_res =
json::LookupOptionalWithResultReturn<double>(json_obj, "top_p");
if (top_p_res.IsErr()) {
return TResult::Error(top_p_res.UnwrapErr());
}
request.top_p = top_p_res.Unwrap();
// max_tokens
Result<std::optional<int64_t>> max_tokens_res =
json::LookupOptionalWithResultReturn<int64_t>(json_obj, "max_tokens");
if (max_tokens_res.IsErr()) {
return TResult::Error(max_tokens_res.UnwrapErr());
}
request.max_tokens = max_tokens_res.Unwrap();
// n
Result<int64_t> n_res = json::LookupOrDefaultWithResultReturn<int64_t>(json_obj, "n", 1);
if (n_res.IsErr()) {
return TResult::Error(n_res.UnwrapErr());
}
request.n = n_res.Unwrap();
// frequency_penalty
Result<std::optional<double>> frequency_penalty_res =
json::LookupOptionalWithResultReturn<double>(json_obj, "frequency_penalty");
if (frequency_penalty_res.IsErr()) {
return TResult::Error(frequency_penalty_res.UnwrapErr());
}
request.frequency_penalty = frequency_penalty_res.Unwrap();
// presence_penalty
Result<std::optional<double>> presence_penalty_res =
json::LookupOptionalWithResultReturn<double>(json_obj, "presence_penalty");
if (presence_penalty_res.IsErr()) {
return TResult::Error(presence_penalty_res.UnwrapErr());
}
request.presence_penalty = presence_penalty_res.Unwrap();
// seed
Result<std::optional<int64_t>> seed_res =
json::LookupOptionalWithResultReturn<int64_t>(json_obj, "seed");
if (seed_res.IsErr()) {
return TResult::Error(seed_res.UnwrapErr());
}
request.seed = seed_res.Unwrap();
// stop strings
Result<std::optional<tvm::ffi::json::Array>> stop_strs_res =
json::LookupOptionalWithResultReturn<tvm::ffi::json::Array>(json_obj, "stop");
if (stop_strs_res.IsErr()) {
return TResult::Error(stop_strs_res.UnwrapErr());
}
std::optional<tvm::ffi::json::Array> stop_strs = stop_strs_res.Unwrap();
if (stop_strs.has_value()) {
std::vector<std::string> stop;
for (const auto& stop_str_value : stop_strs.value()) {
if (!stop_str_value.try_cast<std::string>().has_value()) {
return TResult::Error("One given value in field \"stop\" is not a string.");
}
stop.push_back(stop_str_value.cast<std::string>());
}
request.stop = std::move(stop);
}
// tool_choice
Result<std::string> tool_choice_res =
json::LookupOrDefaultWithResultReturn<std::string>(json_obj, "tool_choice", "auto");
if (tool_choice_res.IsErr()) {
return TResult::Error(tool_choice_res.UnwrapErr());
}
request.tool_choice = tool_choice_res.Unwrap();
// tools
Result<std::optional<tvm::ffi::json::Array>> tools_arr_res =
json::LookupOptionalWithResultReturn<tvm::ffi::json::Array>(json_obj, "tools");
if (tool_choice_res.IsErr()) {
return TResult::Error(tool_choice_res.UnwrapErr());
}
std::optional<tvm::ffi::json::Array> tools_arr = tools_arr_res.Unwrap();
if (tools_arr.has_value()) {
std::vector<ChatTool> tools;
tools.reserve(tools_arr.value().size());
for (const auto& item : tools_arr.value()) {
if (!item.try_cast<tvm::ffi::json::Object>().has_value()) {
return TResult::Error("A tool of the chat completion request is not an object");
}
Result<ChatTool> tool = ChatTool::FromJSON(item.cast<tvm::ffi::json::Object>());
if (tool.IsErr()) {
return TResult::Error(tool.UnwrapErr());
}
tools.push_back(tool.Unwrap());
}
request.tools = tools;
}
// response format
std::optional<tvm::ffi::json::Object> response_format_obj =
json::LookupOptional<tvm::ffi::json::Object>(json_obj, "response_format");
if (response_format_obj.has_value()) {
Result<ResponseFormat> response_format_res =
ResponseFormat::FromJSON(response_format_obj.value());
if (response_format_res.IsErr()) {
return TResult::Error(response_format_res.UnwrapErr());
}
request.response_format = response_format_res.Unwrap();
}
// debug_config
Result<std::optional<tvm::ffi::json::Object>> debug_config_opt_res =
json::LookupOptionalWithResultReturn<tvm::ffi::json::Object>(json_obj, "debug_config");
if (debug_config_opt_res.IsErr()) {
return TResult::Error(debug_config_opt_res.UnwrapErr());
}
auto debug_config_opt = debug_config_opt_res.Unwrap();
if (debug_config_opt.has_value()) {
Result<DebugConfig> debug_config_res = DebugConfig::FromJSON(debug_config_opt.value());
if (debug_config_res.IsErr()) {
return TResult::Error(debug_config_res.UnwrapErr());
}
request.debug_config = debug_config_res.Unwrap();
}
// TODO: Other parameters
return TResult::Ok(request);
}
tvm::ffi::json::Object ChatCompletionMessage::AsJSON() const {
tvm::ffi::json::Object obj;
if (this->content.IsText()) {
obj.Set("content", this->content.Text());
} else if (this->content.IsParts()) {
tvm::ffi::json::Array content_arr;
for (const auto& item : this->content.Parts()) {
tvm::ffi::json::Object item_obj;
for (const auto& pair : item) {
item_obj.Set(pair.first, pair.second);
}
content_arr.push_back(item_obj);
}
obj.Set("content", content_arr);
}
obj.Set("role", this->role);
if (this->name.has_value()) {
obj.Set("name", this->name.value());
}
if (this->tool_call_id.has_value()) {
obj.Set("tool_call_id", this->tool_call_id.value());
}
if (this->tool_calls.has_value()) {
tvm::ffi::json::Array tool_calls_arr;
for (const auto& tool_call : this->tool_calls.value()) {
tool_calls_arr.push_back(tool_call.AsJSON());
}
obj.Set("tool_calls", tool_calls_arr);
}
return obj;
}
tvm::ffi::json::Object ChatCompletionResponseChoice::AsJSON() const {
tvm::ffi::json::Object obj;
if (!this->finish_reason.has_value()) {
obj.Set("finish_reason", nullptr);
} else {
if (this->finish_reason == FinishReason::stop) {
obj.Set("finish_reason", "stop");
} else if (this->finish_reason == FinishReason::length) {
obj.Set("finish_reason", "length");
} else if (this->finish_reason == FinishReason::tool_calls) {
obj.Set("finish_reason", "tool_calls");
} else if (this->finish_reason == FinishReason::error) {
obj.Set("finish_reason", "error");
}
}
obj.Set("index", static_cast<int64_t>(this->index));
obj.Set("message", this->message.AsJSON());
return obj;
}
tvm::ffi::json::Object ChatCompletionStreamResponseChoice::AsJSON() const {
tvm::ffi::json::Object obj;
if (!this->finish_reason.has_value()) {
obj.Set("finish_reason", nullptr);
} else {
if (this->finish_reason.value() == FinishReason::stop) {
obj.Set("finish_reason", "stop");
} else if (this->finish_reason.value() == FinishReason::length) {
obj.Set("finish_reason", "length");
} else if (this->finish_reason.value() == FinishReason::tool_calls) {
obj.Set("finish_reason", "tool_calls");
} else if (this->finish_reason.value() == FinishReason::error) {
obj.Set("finish_reason", "error");
}
}
obj.Set("index", static_cast<int64_t>(this->index));
obj.Set("delta", this->delta.AsJSON());
return obj;
}
tvm::ffi::json::Object ChatCompletionResponse::AsJSON() const {
tvm::ffi::json::Object obj;
obj.Set("id", this->id);
tvm::ffi::json::Array choices_arr;
for (const auto& choice : this->choices) {
choices_arr.push_back(choice.AsJSON());
}
obj.Set("choices", choices_arr);
obj.Set("created", static_cast<int64_t>(this->created));
obj.Set("model", this->model);
obj.Set("system_fingerprint", this->system_fingerprint);
obj.Set("object", this->object);
return obj;
}
tvm::ffi::json::Object ChatCompletionStreamResponse::AsJSON() const {
tvm::ffi::json::Object obj;
obj.Set("id", this->id);
tvm::ffi::json::Array choices_arr;
for (const auto& choice : this->choices) {
choices_arr.push_back(choice.AsJSON());
}
obj.Set("choices", choices_arr);
obj.Set("created", static_cast<int64_t>(this->created));
obj.Set("model", this->model);
obj.Set("system_fingerprint", this->system_fingerprint);
obj.Set("object", this->object);
if (usage.has_value()) {
obj.Set("usage", usage.value());
}
return obj;
}
} // namespace json_ffi
} // namespace llm
} // namespace mlc
+208
View File
@@ -0,0 +1,208 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file json_ffi/openai_api_protocol.h
* \brief The header of OpenAI API Protocol in MLC LLM.
*/
#ifndef MLC_LLM_JSON_FFI_OPENAI_API_PROTOCOL_H
#define MLC_LLM_JSON_FFI_OPENAI_API_PROTOCOL_H
#include <tvm/ffi/extra/json.h>
#include <ctime>
#include <optional>
#include <random>
#include <string>
#include <unordered_map>
#include <vector>
#include "../serve/config.h"
#include "../support/result.h"
namespace mlc {
namespace llm {
namespace json_ffi {
using serve::DebugConfig;
using serve::ResponseFormat;
enum class Type { text, json_object, function };
enum class FinishReason { stop, length, tool_calls, error };
inline std::string GenerateUUID(size_t length) {
auto randchar = []() -> char {
const char charset[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
const size_t max_index = (sizeof(charset) - 1);
return charset[rand() % max_index];
};
std::string str(length, 0);
std::generate_n(str.begin(), length, randchar);
return str;
}
class ChatFunction {
public:
std::optional<std::string> description = std::nullopt;
std::string name;
// Todo: change to std::vector<std::pair<std::string, std::string>>?
std::unordered_map<std::string, std::string>
parameters; // Assuming parameters are string key-value pairs
static Result<ChatFunction> FromJSON(const tvm::ffi::json::Object& json);
tvm::ffi::json::Object AsJSON() const;
};
class ChatTool {
public:
Type type = Type::function;
ChatFunction function;
static Result<ChatTool> FromJSON(const tvm::ffi::json::Object& json);
tvm::ffi::json::Object AsJSON() const;
};
class ChatFunctionCall {
public:
std::string name;
std::optional<std::unordered_map<std::string, std::string>> arguments =
std::nullopt; // Assuming arguments are string key-value pairs
static Result<ChatFunctionCall> FromJSON(const tvm::ffi::json::Object& json);
tvm::ffi::json::Object AsJSON() const;
};
class ChatToolCall {
public:
std::string id = "call_" + GenerateUUID(8);
Type type = Type::function;
ChatFunctionCall function;
static Result<ChatToolCall> FromJSON(const tvm::ffi::json::Object& json);
tvm::ffi::json::Object AsJSON() const;
};
class ChatCompletionMessageContent {
public:
ChatCompletionMessageContent() = default;
ChatCompletionMessageContent(std::nullopt_t) {} // NOLINT(*)
ChatCompletionMessageContent(std::string text) : text_(text) {} // NOLINT(*)
ChatCompletionMessageContent(
std::vector<std::unordered_map<std::string, std::string>> parts) // NOLINT(*)
: parts_(parts) {}
bool IsNull() const { return !IsText() && !IsParts(); }
bool IsText() const { return text_.operator bool(); }
bool IsParts() const { return parts_.operator bool(); }
const std::string& Text() const { return text_.value(); }
const std::vector<std::unordered_map<std::string, std::string>>& Parts() const {
return parts_.value();
}
private:
/*! \brief used to store text content */
std::optional<std::string> text_;
std::optional<std::vector<std::unordered_map<std::string, std::string>>> parts_;
};
class ChatCompletionMessage {
public:
ChatCompletionMessageContent content =
std::nullopt; // Assuming content is a list of string key-value pairs
std::string role;
std::optional<std::string> name = std::nullopt;
std::optional<std::vector<ChatToolCall>> tool_calls = std::nullopt;
std::optional<std::string> tool_call_id = std::nullopt;
static Result<ChatCompletionMessage> FromJSON(const tvm::ffi::json::Object& json);
tvm::ffi::json::Object AsJSON() const;
};
class ChatCompletionRequest {
public:
std::vector<ChatCompletionMessage> messages;
std::optional<std::string> model = std::nullopt;
std::optional<double> frequency_penalty = std::nullopt;
std::optional<double> presence_penalty = std::nullopt;
bool logprobs = false;
int top_logprobs = 0;
std::optional<std::vector<std::pair<int, float>>> logit_bias = std::nullopt;
std::optional<int> max_tokens = std::nullopt;
int n = 1;
std::optional<int> seed = std::nullopt;
std::optional<std::vector<std::string>> stop = std::nullopt;
bool stream = false;
std::optional<double> temperature = std::nullopt;
std::optional<double> top_p = std::nullopt;
std::optional<std::vector<ChatTool>> tools = std::nullopt;
std::optional<std::string> tool_choice = std::nullopt;
std::optional<std::string> user = std::nullopt;
bool ignore_eos = false;
std::optional<ResponseFormat> response_format = std::nullopt;
std::optional<DebugConfig> debug_config = std::nullopt;
/*! \brief Parse and create a ChatCompletionRequest instance from the given JSON string. */
static Result<ChatCompletionRequest> FromJSON(const std::string& json_str);
// TODO: check_penalty_range, check_logit_bias, check_logprobs
};
class ChatCompletionResponseChoice {
public:
std::optional<FinishReason> finish_reason;
int index = 0;
ChatCompletionMessage message;
// TODO: logprobs
tvm::ffi::json::Object AsJSON() const;
};
class ChatCompletionStreamResponseChoice {
public:
std::optional<FinishReason> finish_reason;
int index = 0;
ChatCompletionMessage delta;
// TODO: logprobs
tvm::ffi::json::Object AsJSON() const;
};
class ChatCompletionResponse {
public:
std::string id;
std::vector<ChatCompletionResponseChoice> choices;
int created = static_cast<int>(std::time(nullptr));
std::string model;
std::string system_fingerprint;
std::string object = "chat.completion";
// TODO: usage_info
tvm::ffi::json::Object AsJSON() const;
};
class ChatCompletionStreamResponse {
public:
std::string id;
std::vector<ChatCompletionStreamResponseChoice> choices;
int created = static_cast<int>(std::time(nullptr));
std::string model;
std::string system_fingerprint;
std::string object = "chat.completion.chunk";
std::optional<tvm::ffi::json::Value> usage;
tvm::ffi::json::Object AsJSON() const;
};
} // namespace json_ffi
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_JSON_FFI_OPENAI_API_PROTOCOL_H
+155
View File
@@ -0,0 +1,155 @@
#include "./model.h"
#include <unordered_map>
#include "../support/json_parser.h"
namespace mlc {
namespace llm {
using namespace tvm::runtime;
using tvm::ffi::Function;
using tvm::ffi::Object;
using tvm::ffi::Optional;
ModelMetadata::Param::Preproc ModelMetadata::Param::Preproc::FromJSON(
const tvm::ffi::json::Object& js, const tvm::ffi::json::Object& model_config) {
Preproc preproc;
TVM_FFI_ICHECK_GE(js.size(), 3) << "ValueError: Invalid preprocessing info in JSON";
preproc.func_name = json::Lookup<std::string>(js, "func_name");
json::SymShapeTuple sym_out_shape = json::Lookup<json::SymShapeTuple>(js, "out_shape");
preproc.out_shape = sym_out_shape.ToStatic(model_config);
json::SymShapeTuple sym_in_shape =
json::LookupOrDefault<json::SymShapeTuple>(js, "in_shape", sym_out_shape);
preproc.in_shape = sym_in_shape.ToStatic(model_config);
preproc.out_dtype = json::Lookup<DLDataType>(js, "out_dtype");
return preproc;
}
ModelMetadata::Param ModelMetadata::Param::FromJSON(const tvm::ffi::json::Object& param,
const tvm::ffi::json::Object& model_config) {
Param result;
result.name = json::Lookup<std::string>(param, "name");
result.dtype = json::Lookup<DLDataType>(param, "dtype");
// A shape being `-1` means that it is dynamic
json::SymShapeTuple sym_shape = json::Lookup<json::SymShapeTuple>(param, "shape");
result.shape = sym_shape.ToStatic(model_config);
// - "preproc"
tvm::ffi::json::Array preprocs = json::Lookup<tvm::ffi::json::Array>(param, "preprocs");
result.preprocs.reserve(preprocs.size());
for (int i = 0; i < preprocs.size(); i++) {
result.preprocs.emplace_back(ModelMetadata::Param::Preproc::FromJSON(
json::Lookup<tvm::ffi::json::Object>(preprocs, i), model_config));
}
// - "pipeline_stages"
int pipeline_parallel_stages =
json::LookupOrDefault<int64_t>(model_config, "pipeline_parallel_stages", 1);
std::optional<tvm::ffi::json::Array> opt_pipeline_stages =
json::LookupOptional<tvm::ffi::json::Array>(param, "pipeline_stages");
if (pipeline_parallel_stages > 1) {
TVM_FFI_ICHECK(opt_pipeline_stages.has_value())
<< "The pipeline stage is undefined for parameter \"" << result.name
<< "\" when the number of pipeline parallel stages is " << pipeline_parallel_stages;
}
if (opt_pipeline_stages.has_value()) {
result.pipeline_stages.reserve(opt_pipeline_stages.value().size());
for (const tvm::ffi::json::Value& v : opt_pipeline_stages.value()) {
auto int_opt = v.try_cast<int64_t>();
TVM_FFI_ICHECK(int_opt.has_value()) << "Pipeline stage is not a integer.";
result.pipeline_stages.push_back(*int_opt);
}
} else {
result.pipeline_stages = {0};
}
return result;
}
ModelMetadata::KVCacheMetadata ModelMetadata::KVCacheMetadata::FromJSON(
const tvm::ffi::json::Object& json) {
KVCacheMetadata kv_cache_metadata;
kv_cache_metadata.num_hidden_layers = json::Lookup<int64_t>(json, "num_hidden_layers");
kv_cache_metadata.head_dim = json::Lookup<int64_t>(json, "head_dim");
kv_cache_metadata.num_attention_heads = json::Lookup<int64_t>(json, "num_attention_heads");
kv_cache_metadata.num_key_value_heads = json::Lookup<int64_t>(json, "num_key_value_heads");
return kv_cache_metadata;
}
ModelMetadata ModelMetadata::FromJSON(const tvm::ffi::json::Object& metadata,
const tvm::ffi::json::Object& model_config) {
ModelMetadata result;
result.model_type = json::Lookup<std::string>(metadata, "model_type");
result.quantization = json::Lookup<std::string>(metadata, "quantization");
result.context_window_size = json::Lookup<int64_t>(metadata, "context_window_size");
result.prefill_chunk_size = json::Lookup<int64_t>(metadata, "prefill_chunk_size");
result.max_batch_size = json::Lookup<int64_t>(metadata, "max_batch_size");
if (metadata.count("sliding_window_size"))
result.sliding_window_size = json::Lookup<int64_t>(metadata, "sliding_window_size");
if (metadata.count("sliding_window")) // to be removed after SLM migration
result.sliding_window_size = json::Lookup<int64_t>(metadata, "sliding_window");
if (metadata.count("attention_sink_size")) // remove after sink is decoupled from model lib
result.attention_sink_size = json::Lookup<int64_t>(metadata, "attention_sink_size");
result.seqlen_padding_factor =
json::LookupOrDefault<int64_t>(metadata, "seqlen_padding_factor", 1);
result.tensor_parallel_shards = json::Lookup<int64_t>(metadata, "tensor_parallel_shards");
result.pipeline_parallel_stages =
json::LookupOrDefault<int64_t>(metadata, "pipeline_parallel_stages", 1);
result.disaggregation = json::LookupOrDefault<bool>(metadata, "disaggregation", false);
result.model_task = json::LookupOrDefault<std::string>(metadata, "model_task", "chat");
if (metadata.count("embedding_metadata")) {
tvm::ffi::json::Object emb =
json::Lookup<tvm::ffi::json::Object>(metadata, "embedding_metadata");
result.embedding_model_type = json::LookupOrDefault<std::string>(emb, "model_type", "");
result.embedding_pooling_strategy =
json::LookupOrDefault<std::string>(emb, "pooling_strategy", "");
result.embedding_normalize = json::LookupOrDefault<bool>(emb, "normalize", false);
}
result.kv_state_kind = KVStateKindFromString(
json::LookupOrDefault<std::string>(metadata, "kv_state_kind", "kv_cache"));
if (result.kv_state_kind != KVStateKind::kNone &&
result.kv_state_kind != KVStateKind::kRNNState) {
result.kv_cache_metadata =
KVCacheMetadata::FromJSON(json::Lookup<tvm::ffi::json::Object>(metadata, "kv_cache"));
} else {
result.kv_cache_metadata = {/*num_hidden_layers=*/0,
/*head_dim=*/0,
/*num_attention_heads=*/0,
/*num_key_value_heads=*/0};
}
{
std::vector<ModelMetadata::Param>& params = result.params;
tvm::ffi::json::Array json_params = json::Lookup<tvm::ffi::json::Array>(metadata, "params");
params.reserve(json_params.size());
for (int i = 0, n = json_params.size(); i < n; ++i) {
params.emplace_back(ModelMetadata::Param::FromJSON(
json::Lookup<tvm::ffi::json::Object>(json_params, i), model_config));
}
}
{
std::unordered_map<std::string, int64_t>& memory_usage = result.memory_usage;
tvm::ffi::json::Object json_memory_usage =
json::Lookup<tvm::ffi::json::Object>(metadata, "memory_usage");
memory_usage.reserve(json_memory_usage.size());
for (const auto& [key, val] : json_memory_usage) {
std::string func_name = key.cast<tvm::ffi::String>();
memory_usage[func_name] = json::Lookup<int64_t>(json_memory_usage, func_name);
}
}
return result;
}
ModelMetadata ModelMetadata::FromModule(Module module, const tvm::ffi::json::Object& model_config) {
std::string json_str = "";
Optional<Function> pf = module->GetFunction("_metadata");
TVM_FFI_ICHECK(pf.has_value()) << "ValueError: _metadata function not found in module";
json_str = pf.value()().cast<String>();
tvm::ffi::json::Object json = json::ParseToJSONObject(json_str);
try {
return ModelMetadata::FromJSON(json, model_config);
} catch (const std::exception& e) {
LOG(WARNING) << "Failed to parse metadata:\n" << json_str << "\nerror: " << e.what();
throw e;
}
}
} // namespace llm
} // namespace mlc
+115
View File
@@ -0,0 +1,115 @@
/*!
* \file model.h
* \brief Metadata stored in model lib
*/
#ifndef MLC_LLM_CPP_MODEL_METADATA_H_
#define MLC_LLM_CPP_MODEL_METADATA_H_
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/dtype.h>
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/extra/module.h>
#include <tvm/ffi/string.h>
#include <tvm/runtime/logging.h>
#include <unordered_map>
namespace mlc {
namespace llm {
using tvm::ffi::Module;
using tvm::ffi::Shape;
using tvm::ffi::String;
/*! \brief The kind of cache. */
enum class KVStateKind : int {
kKVCache = 0,
kRNNState = 1,
kNone = 2,
kHybrid = 3,
};
inline std::string KVStateKindToString(KVStateKind kv_state_kind) {
if (kv_state_kind == KVStateKind::kKVCache) {
return "kv_cache";
} else if (kv_state_kind == KVStateKind::kRNNState) {
return "rnn_state";
} else if (kv_state_kind == KVStateKind::kNone) {
return "none";
} else if (kv_state_kind == KVStateKind::kHybrid) {
return "hybrid";
} else {
LOG(FATAL) << "Invalid kv state kind: " << static_cast<int>(kv_state_kind);
}
}
inline KVStateKind KVStateKindFromString(const std::string& kv_state_kind) {
if (kv_state_kind == "kv_cache") {
return KVStateKind::kKVCache;
} else if (kv_state_kind == "rnn_state") {
return KVStateKind::kRNNState;
} else if (kv_state_kind == "none") {
return KVStateKind::kNone;
} else if (kv_state_kind == "hybrid") {
return KVStateKind::kHybrid;
} else {
LOG(FATAL) << "Invalid kv state kind string: " << kv_state_kind;
}
}
struct ModelMetadata {
struct Param {
struct Preproc {
String func_name;
Shape in_shape;
Shape out_shape;
DLDataType out_dtype;
static Preproc FromJSON(const tvm::ffi::json::Object& js,
const tvm::ffi::json::Object& model_config);
};
String name;
Shape shape;
DLDataType dtype;
std::vector<Preproc> preprocs;
std::vector<int> pipeline_stages;
static Param FromJSON(const tvm::ffi::json::Object& param_obj,
const tvm::ffi::json::Object& model_config);
};
struct KVCacheMetadata {
int64_t num_hidden_layers;
int64_t num_attention_heads;
int64_t num_key_value_heads;
int64_t head_dim;
static KVCacheMetadata FromJSON(const tvm::ffi::json::Object& json);
};
std::string model_type;
std::string quantization;
int64_t context_window_size;
int64_t prefill_chunk_size;
int64_t max_batch_size;
int64_t sliding_window_size;
int64_t tensor_parallel_shards;
int64_t pipeline_parallel_stages;
bool disaggregation;
int64_t attention_sink_size;
int64_t seqlen_padding_factor;
std::vector<Param> params;
std::unordered_map<std::string, int64_t> memory_usage;
KVStateKind kv_state_kind;
KVCacheMetadata kv_cache_metadata;
std::string model_task;
std::string embedding_model_type;
std::string embedding_pooling_strategy;
bool embedding_normalize = false;
static ModelMetadata FromJSON(const tvm::ffi::json::Object& json_str,
const tvm::ffi::json::Object& model_config);
static ModelMetadata FromModule(Module module, const tvm::ffi::json::Object& model_config);
};
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_CPP_MODEL_METADATA_H_
+100
View File
@@ -0,0 +1,100 @@
/*!
* \file builtin.cc
* \brief Multi-GPU builtin functions in MLC LLM.
*/
#ifndef MLC_SINGLE_GPU_ONLY
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/optional.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/runtime/disco/builtin.h>
#include <tvm/runtime/disco/disco_worker.h>
#include <tvm/runtime/tensor.h>
#include <tvm/runtime/vm/vm.h>
namespace mlc {
namespace llm {
namespace multi_gpu {
using namespace tvm::runtime;
using tvm::ffi::Array;
using tvm::ffi::ObjectRef;
using tvm::ffi::Optional;
using tvm::ffi::Shape;
ObjectRef DispatchFunctionByGroup(tvm::ffi::AnyView vm_arg,
Array<Array<ObjectRef>> funcs_and_args) {
using namespace vm;
VirtualMachine* vm = VirtualMachine::GetContextPtr(vm_arg);
DiscoWorker* worker = DiscoWorker::ThreadLocal();
int world_size = worker->num_workers;
int group_size = worker->num_workers / worker->num_groups;
int num_group = world_size / group_size;
TVM_FFI_ICHECK_EQ(funcs_and_args.size(), num_group)
<< "Number of groups mismatches. There are " << num_group
<< " groups while the function/arg array has " << funcs_and_args.size() << " elements.";
int group_id = worker->worker_id / group_size;
TVM_FFI_ICHECK(!funcs_and_args[group_id].empty())
<< "No function is provided for group " << group_id;
VMClosure func = funcs_and_args[group_id][0].as_or_throw<VMClosure>();
int num_args = static_cast<int>(funcs_and_args[group_id].size()) - 1;
std::vector<tvm::ffi::AnyView> packed_args(num_args);
for (int i = 0; i < num_args; ++i) {
// NOTE: Need explicily define `arg` so that the argument does not
// have type code kTVMObjectRValueRefArg.
packed_args[i] = funcs_and_args[group_id][1 + i];
}
tvm::ffi::Any rv;
vm->InvokeClosurePacked(funcs_and_args[group_id][0].as_or_throw<VMClosure>(),
tvm::ffi::PackedArgs(packed_args.data(), packed_args.size()), &rv);
return rv.cast<ObjectRef>();
}
ObjectRef SendFromLastGroupToWorker0(Tensor send, Optional<Tensor> recv, Shape shape,
DLDataType dtype) {
DiscoWorker* worker = DiscoWorker::ThreadLocal();
int worker_id = worker->worker_id;
int world_size = worker->num_workers;
int group_size = worker->num_workers / worker->num_groups;
TVM_FFI_ICHECK_NE(world_size, group_size) << "Cannot perform when there is only one group.";
int sender_id = world_size - group_size;
if (worker_id == 0) {
TVM_FFI_ICHECK(recv.has_value()) << "The receive Tensor is undefined for worker 0.";
Tensor recv_arr = recv.value().CreateView(shape, dtype);
RecvFromWorker(recv_arr, sender_id);
return recv_arr;
} else if (worker_id == sender_id) {
TVM_FFI_ICHECK_EQ(send->dtype, dtype)
<< "The src Tensor has mismatched dtype than the expected dtype.";
TVM_FFI_ICHECK_EQ(send->ndim, shape.size())
<< "The src Tensor has mismatched shape than the expected shape.";
for (int i = 0; i < send->ndim; ++i) {
TVM_FFI_ICHECK_EQ(send->shape[i], shape[i])
<< "The src Tensor has mismatched shape than the expected shape.";
}
SendToWorker(send, /*receiver_id=*/0);
return recv.value_or(Tensor(nullptr));
}
// We only process for worker 0 and the first worker of the last group.
// For other workers, we return the input object.
return recv.value_or(Tensor(nullptr));
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("mlc.multi_gpu.DispatchFunctionByGroup", DispatchFunctionByGroup)
.def("mlc.multi_gpu.SendFromLastGroupToWorker0", SendFromLastGroupToWorker0);
}
} // namespace multi_gpu
} // namespace llm
} // namespace mlc
#endif // MLC_SINGLE_GPU_ONLY
+327
View File
@@ -0,0 +1,327 @@
/*!
* \file multi_gpu_loader.cc
* \brief Implementation of a multi-GPU loader with loading-time sharding.
*/
#ifndef MLC_SINGLE_GPU_ONLY
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/optional.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/runtime/device_api.h>
#include <tvm/runtime/disco/builtin.h>
#include <tvm/runtime/disco/disco_worker.h>
#include <tvm/runtime/vm/tensor_cache_support.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <functional>
#include <numeric>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#include "../metadata/model.h"
#include "../support/progress_bar.h"
namespace mlc {
namespace llm {
namespace multi_gpu {
using tvm::Device;
using tvm::runtime::vm::TensorCacheMetadata;
using namespace tvm::runtime;
using tvm::ffi::Array;
using tvm::ffi::Function;
using tvm::ffi::Object;
using tvm::ffi::Optional;
using tvm::ffi::Shape;
using tvm::ffi::TypedFunction;
using DurationType = std::chrono::microseconds;
class RangeTimer {
public:
explicit RangeTimer(DurationType* result)
: start(std::chrono::high_resolution_clock::now()), result(result) {}
~RangeTimer() {
std::chrono::time_point<std::chrono::high_resolution_clock> end =
std::chrono::high_resolution_clock::now(); //
auto duration = end - start;
(*result) += std::chrono::duration_cast<DurationType>(end - start);
}
private:
std::chrono::time_point<std::chrono::high_resolution_clock> start;
DurationType* result;
};
class PreprocessorPool {
public:
explicit PreprocessorPool(const ModelMetadata& model_metadata, Module vm_module) {
for (const ModelMetadata::Param& param : model_metadata.params) {
for (const ModelMetadata::Param::Preproc& preproc : param.preprocs) {
const std::string& func_name = preproc.func_name;
if (Function f = vm_module.defined()
? vm_module->GetFunction(func_name, true).value_or(Function(nullptr))
: nullptr;
f != nullptr) {
preproc_funcs[func_name] = f;
} else if (const auto f = Function::GetGlobal(func_name); f.has_value()) {
preproc_funcs[func_name] = *f;
} else {
LOG(FATAL) << "ValueError: Undefined function: " << func_name;
}
}
}
}
Tensor Apply(Tensor param, const ModelMetadata::Param& param_info) const {
for (const ModelMetadata::Param::Preproc& preproc : param_info.preprocs) {
const std::string& func_name = preproc.func_name;
Tensor param_in = param;
param = Tensor::Empty(preproc.out_shape, preproc.out_dtype, param->device);
TVM_FFI_ICHECK(preproc_funcs.count(func_name));
DLTensor dl_param_in = *param_in.operator->();
DLTensor dl_param = *param.operator->();
preproc_funcs.at(func_name)(&dl_param_in, &dl_param);
}
return param;
}
private:
std::unordered_map<std::string, TypedFunction<void(DLTensor*, DLTensor*)>> preproc_funcs;
};
struct ParamInfo {
const TensorCacheMetadata::FileRecord* file;
const TensorCacheMetadata::FileRecord::ParamRecord* param;
};
Tensor RecvFromGlobalWorker0(Device device, const ModelMetadata::Param& param_info) {
Shape shape = param_info.preprocs.empty() ? param_info.shape : param_info.preprocs[0].in_shape;
Tensor result = Tensor::Empty(shape, param_info.dtype, device);
RecvFromWorker0(result);
return result;
}
Tensor BroadcastOrShardAndScatter(Tensor param, const ModelMetadata::Param& param_info,
int num_shards, const PreprocessorPool& preprocs) {
bool needs_sharding = !param_info.preprocs.empty();
if (!needs_sharding) {
BroadcastFromWorker0(param, /*in_group=*/true, param);
return param;
}
Device device = param->device;
Shape shape = param_info.preprocs.back().out_shape;
DLDataType dtype = param_info.preprocs.back().out_dtype;
TVM_FFI_ICHECK(shape.size() >= 1 && shape[0] == num_shards)
<< "ValueError: The first dimension of the output shape must be equal to the "
<< "number of shards, but got: " << shape << " and num_shards = " << num_shards;
param = preprocs.Apply(param, param_info);
Tensor result = Tensor::Empty(Shape(shape.begin() + 1, shape.end()), dtype, device);
ScatterFromWorker0(param, /*in_group=*/true, result);
return result;
}
Tensor ReceiveBroadcastedOrSharded(Device device, const ModelMetadata::Param& param_info,
int num_shards) {
bool needs_sharding = !param_info.preprocs.empty();
Tensor result;
if (needs_sharding) {
Shape shape = param_info.preprocs.back().out_shape;
DLDataType dtype = param_info.preprocs.back().out_dtype;
result = Tensor::Empty(Shape(shape.begin() + 1, shape.end()), dtype, device);
ScatterFromWorker0(std::nullopt, /*in_group=*/true, result);
} else {
result = Tensor::Empty(param_info.shape, param_info.dtype, device);
BroadcastFromWorker0(result, /*in_group=*/true, result);
}
return result;
}
std::string FormatDuration(DurationType duration) {
std::ostringstream os;
auto float_seconds = std::chrono::duration_cast<std::chrono::duration<float>>(duration).count();
os << std::fixed << std::setprecision(3) << float_seconds << " s";
return os.str();
}
Array<Optional<Tensor>> LoadMultiGPU(const std::string& model_path, Module vm_module,
const std::string& model_config_str) {
DiscoWorker* worker = DiscoWorker::ThreadLocal();
Device device = worker->default_device;
int worker_id = worker->worker_id;
int group_size = worker->num_workers / worker->num_groups;
int num_shards = group_size;
int group_id = worker_id / group_size;
LOG(INFO) << "[Worker #" << worker_id << "] Loading model to device: " << device;
// Step 0. Initialize metadata and paths
TensorCacheMetadata tensor_cache_metadata = TensorCacheMetadata::Load(model_path);
tvm::ffi::json::Value model_config = tvm::ffi::json::Parse(model_config_str);
ModelMetadata model_metadata =
ModelMetadata::FromModule(vm_module, model_config.cast<tvm::ffi::json::Object>());
TVM_FFI_ICHECK_EQ(model_metadata.tensor_parallel_shards, num_shards)
<< "ValueError: The model is compiled using `--tensor-parallel-shards="
<< model_metadata.tensor_parallel_shards
<< "`, but mlc-chat-config.json is configured to use " << num_shards << " GPUs. "
<< "Please set \"tensor_parallel_shards\" in mlc-chat-config.json to "
<< model_metadata.tensor_parallel_shards;
// Step 1. Extract auxiliary information
PreprocessorPool preprocs(model_metadata, vm_module);
std::unordered_map<std::string, ModelMetadata::Param> param_name2info;
for (const ModelMetadata::Param& param : model_metadata.params) {
param_name2info[param.name] = param;
}
// Step 2. Load, preprocess and shard all the parameters
std::unordered_map<std::string, Tensor> sharded_params;
if (worker_id == 0) {
DurationType time_loading(0);
DurationType time_preproc(0);
ProgressBar progress_bar(model_metadata.params.size());
LOG(INFO) << "Loading parameters...";
for (const TensorCacheMetadata::FileRecord& record : tensor_cache_metadata.records) {
Array<Tensor> loaded_params;
{
RangeTimer _(&time_loading);
std::string raw_data_buffer;
loaded_params = record.Load(device, model_path, &raw_data_buffer);
DeviceAPI::Get(device)->StreamSync(device, nullptr);
}
// For each parameter in the shard file, preprocess and shard it
for (size_t i = 0; i < record.records.size(); ++i, progress_bar.Progress()) {
RangeTimer _(&time_preproc);
const std::string& param_name = record.records[i].name;
const ModelMetadata::Param& param_info = param_name2info.at(param_name);
for (int group_id : param_info.pipeline_stages) {
if (group_id == 0) {
// Broadcast or shard-scatter this parameter to all workers in worker group 0.
sharded_params[param_name] =
BroadcastOrShardAndScatter(loaded_params[i], param_info, num_shards, preprocs);
} else {
// Send this parameter to the first worker of the worker group of "group_id",
// and let that first worker to process this parameter.
SendToWorker(loaded_params[i], /*receiver_id=*/group_id * group_size);
}
}
DeviceAPI::Get(device)->StreamSync(device, nullptr);
}
}
LOG(INFO) << "Loading done. Time used:" << std::fixed << std::setprecision(3) //
<< " Loading " << FormatDuration(time_loading) << " Preprocessing "
<< FormatDuration(time_preproc) << ".";
} else {
for (const TensorCacheMetadata::FileRecord& record : tensor_cache_metadata.records) {
for (size_t i = 0; i < record.records.size(); ++i) {
const std::string& param_name = record.records[i].name;
const ModelMetadata::Param& param_info = param_name2info.at(param_name);
if (std::find(param_info.pipeline_stages.begin(), param_info.pipeline_stages.end(),
group_id) == param_info.pipeline_stages.end()) {
// This worker group doesn't need to hold a copy of this parameter.
continue;
}
if (worker_id % group_size == 0) {
// The worker is the first worker of its worker group (while not the first worker group).
// Receive the full parameter from the global worker 0.
Tensor full_param = RecvFromGlobalWorker0(device, param_info);
// Broadcast or shard-scatter this parameter to all workers in its worker group.
sharded_params[param_name] =
BroadcastOrShardAndScatter(full_param, param_info, num_shards, preprocs);
} else {
// The worker is not the first worker of its worker group.
// Receive from the first worker in the its worker group.
sharded_params[param_name] = ReceiveBroadcastedOrSharded(device, param_info, num_shards);
}
}
}
}
// Step 3. Reorder the sharded parameters according to the order in model_metadata
Array<Optional<Tensor>> shards;
shards.reserve(model_metadata.params.size());
for (const ModelMetadata::Param& param : model_metadata.params) {
const auto& it = sharded_params.find(param.name);
shards.push_back(it == sharded_params.end() ? Optional<Tensor>() : it->second);
}
return shards;
}
Array<Optional<Tensor>> LoadMultiGPUPresharded(const std::string& model_path, Module vm_module,
const std::string& model_config_str) {
DiscoWorker* worker = DiscoWorker::ThreadLocal();
Device device = worker->default_device;
int worker_id = worker->worker_id;
int group_size = worker->num_workers / worker->num_groups;
int num_shards = group_size;
int group_id = worker_id / group_size;
int local_worker_id = worker_id % group_size;
LOG(INFO) << "[Worker #" << worker_id << "] Loading model to device: " << device;
// Step 0. Initialize metadata and paths
TensorCacheMetadata tensor_cache_metadata = TensorCacheMetadata::Load(model_path);
tvm::ffi::json::Value model_config = tvm::ffi::json::Parse(model_config_str);
ModelMetadata model_metadata =
ModelMetadata::FromModule(vm_module, model_config.cast<tvm::ffi::json::Object>());
std::unordered_map<std::string, ParamInfo> param_info_map;
for (const TensorCacheMetadata::FileRecord& file_record : tensor_cache_metadata.records) {
for (const TensorCacheMetadata::FileRecord::ParamRecord& param_record : file_record.records) {
const std::string& param_name = param_record.name;
param_info_map[param_name] = ParamInfo{&file_record, &param_record};
}
}
Array<Optional<Tensor>> params;
const TensorCacheMetadata::FileRecord* current_file_;
std::string current_file_stream_;
params.reserve(model_metadata.params.size());
DurationType time_loading(0);
for (const ModelMetadata::Param& param : model_metadata.params) {
RangeTimer _(&time_loading);
if (std::find(param.pipeline_stages.begin(), param.pipeline_stages.end(), group_id) ==
param.pipeline_stages.end()) {
// This worker group doesn't need to hold a copy of this parameter.
params.push_back(Optional<Tensor>());
continue;
}
bool needs_sharding = !param.preprocs.empty();
std::string param_name =
needs_sharding ? static_cast<const std::stringstream&>(
std::stringstream() << param.name << "_shard-" << local_worker_id)
.str()
: std::string(param.name);
auto it = param_info_map.find(param_name);
TVM_FFI_ICHECK(it != param_info_map.end())
<< "ValueError: Cannot find parameter: " << param_name;
const ParamInfo& param_info = (*it).second;
const TensorCacheMetadata::FileRecord::ParamRecord* param_record = param_info.param;
const TensorCacheMetadata::FileRecord* file_record = param_info.file;
if (file_record != current_file_) {
current_file_ = file_record;
file_record->Load(device, model_path, &current_file_stream_);
}
params.push_back(param_record->Load(device, &current_file_stream_));
}
SyncWorker();
if (worker_id == 0) {
LOG(INFO) << "Loading done. Time used: " << FormatDuration(time_loading) << ".";
}
return params;
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("mlc.multi_gpu.LoadMultiGPU", LoadMultiGPU)
.def("mlc.multi_gpu.LoadMultiGPUPresharded", LoadMultiGPUPresharded);
}
} // namespace multi_gpu
} // namespace llm
} // namespace mlc
#endif // MLC_SINGLE_GPU_ONLY
+1082
View File
File diff suppressed because it is too large Load Diff
+470
View File
@@ -0,0 +1,470 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/config.h
*/
#ifndef MLC_LLM_SERVE_CONFIG_H_
#define MLC_LLM_SERVE_CONFIG_H_
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
#include <tvm/runtime/device_api.h>
#include <optional>
#include "../metadata/model.h"
#include "../support/result.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm;
using namespace tvm::runtime;
using tvm::ffi::Array;
using tvm::ffi::Object;
using tvm::ffi::ObjectPtr;
using tvm::ffi::ObjectRef;
using tvm::ffi::Optional;
using tvm::ffi::Shape;
using tvm::ffi::String;
/****************** GenerationConfig ******************/
/*! \brief The response format of a request. */
struct ResponseFormat {
String type = "text";
Optional<String> schema = std::nullopt;
/*!
* \brief Create debug config from JSON.
* \param config_json The json string for generation config
* \returns The converted result.
*/
static Result<ResponseFormat> FromJSON(const tvm::ffi::json::Object& config_json);
/**
* \return serialized json value of the config.
*/
tvm::ffi::json::Object AsJSON() const;
};
enum class SpecialRequestKind : int {
kNone = 0,
kQueryEngineMetrics = 1,
};
enum class DisaggRequestKind : int {
kNone = 0,
kPrepareReceive = 1,
kRemoteSend = 2,
kStartGeneration = 3,
};
/*! \brief Controls the behavior of inference with grammar constraint. */
enum class GrammarExecutionMode : int {
/*! \brief If grammar is provided for a request, use the grammar to constrain the output token. */
kConstraint = 0,
/*! \brief If grammar is provided for a request, not only constrain the output, but also use the
* jump-forward decoding to predict the next tokens. This is the default option. */
kJumpForward = 1,
};
/*! \brief The config for disaggregation requests. */
class DisaggConfig {
public:
DisaggRequestKind kind = DisaggRequestKind::kNone;
std::vector<Shape> kv_append_metadata;
// "kv_window_begin" and "kv_window_end" denote the KV interval of interests.
// "kv_window_end" supports Python style negative indexing.
// The concrete meaning varies for different special request kind:
// - For "prepare_receive", the begin is always 0, and "[0:end]" denotes
// the KV range to prefill on a prefill instance.
// - For "remote_send", "[begin:end]" means the KV range to compute prefill
// and send to the decode instance.
// - For "start_generation", the end is always nullopt, and "[begin:]" denotes
// the KV range to prefill locally on the decode instance.
std::optional<int> kv_window_begin = std::nullopt;
std::optional<int> kv_window_end = std::nullopt;
std::optional<int> dst_group_offset = std::nullopt;
static Result<DisaggConfig> FromJSON(const tvm::ffi::json::Object& config_json);
tvm::ffi::json::Object AsJSON() const;
};
/*! \brief The debug configuration of a request. */
class DebugConfig {
public:
bool ignore_eos = false;
bool pinned_system_prompt = false;
SpecialRequestKind special_request = SpecialRequestKind::kNone;
/*! \brief The grammar execution mode. */
GrammarExecutionMode grammar_execution_mode = GrammarExecutionMode::kJumpForward;
DisaggConfig disagg_config;
/*!
* \brief Create debug config from JSON.
* \param config_json The json string for generation config
* \returns The converted result.
*/
static Result<DebugConfig> FromJSON(const tvm::ffi::json::Object& config_json);
/**
* \return serialized json value of the config.
*/
tvm::ffi::json::Object AsJSON() const;
};
/*! \brief The generation configuration of a request. */
class GenerationConfigNode : public Object {
public:
int n = 1;
double temperature = 1.0;
double top_p = 1.0;
double frequency_penalty = 0.0;
double presence_penalty = 0.0;
double repetition_penalty = 1.0;
bool logprobs = false;
int top_logprobs = 0;
std::vector<std::pair<int, float>> logit_bias;
int seed;
// -1 means infinite
int max_tokens = -1;
Array<String> stop_strs;
std::vector<int> stop_token_ids;
ResponseFormat response_format;
DebugConfig debug_config;
tvm::ffi::json::Object AsJSON() const;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<GenerationConfigNode>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.GenerationConfig", GenerationConfigNode, Object);
};
class GenerationConfig : public ObjectRef {
public:
/*!
* \brief Run validation of generation config and ensure values are in bound.
* \return The validtaed Generation config or error.
*/
static Result<GenerationConfig> Validate(GenerationConfig cfg);
/*!
* \brief Create generation config from JSON.
* \param config_json The json string for generation config
* \param default_config The default config
*/
static Result<GenerationConfig> FromJSON(const tvm::ffi::json::Object& config_json,
const GenerationConfig& default_config);
/*! \brief Get the default generation config from the model config. */
static GenerationConfig GetDefaultFromModelConfig(const tvm::ffi::json::Object& json);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(GenerationConfig, ObjectRef, GenerationConfigNode);
};
/****************** Engine config ******************/
/*!
* \brief The engine mode in MLC LLM.
* We provide three preset modes: "local", "interactive" and "server".
* The default mode is "local".
* The choice of mode decides the values of "max_batch_size", "max_total_sequence_length"
* and "prefill_chunk_size" when they are not explicitly specified.
* 1. Mode "local" refers to the local server deployment which has low
* request concurrency. So the max batch size will be set to 4, and max
* total sequence length and prefill chunk size are set to the context
* window size (or sliding window size) of the model.
* 2. Mode "interactive" refers to the interactive use of server, which
* has at most 1 concurrent request. So the max batch size will be set to 1,
* and max total sequence length and prefill chunk size are set to the context
* window size (or sliding window size) of the model.
* 3. Mode "server" refers to the large server use case which may handle
* many concurrent request and want to use GPU memory as much as possible.
* In this mode, we will automatically infer the largest possible max batch
* size and max total sequence length.
*/
enum class EngineMode : int {
kLocal = 0,
kInteractive = 1,
kServer = 2,
};
/*! \brief The prefix cache mode. */
enum class PrefixCacheMode : int {
/*! \brief Disable prefix cache. */
kDisable = 0,
/*! \brief The paged radix tree based prefix cache mode. */
kRadix = 1,
};
/*! \brief The speculative mode. */
enum class SpeculativeMode : int {
/*! \brief Disable speculative decoding. */
kDisable = 0,
/*! \brief The normal speculative decoding (small draft) mode. */
kSmallDraft = 1,
/*! \brief The eagle-style speculative decoding. */
kEagle = 2,
/*! \brief The Medusa-style speculative decoding. */
kMedusa = 3,
};
/*! \brief The prefill mode. */
enum class PrefillMode : int {
/*! \brief Only chunked prefill is enabled. */
kChunked = 0,
/*!
* \brief The hybrid prefill or split-fuse prefill is enabled, some decode steps will be fused
* to prefill
*/
kHybrid = 1,
};
class InferrableEngineConfig;
/*! \brief The configuration of engine execution config. */
class EngineConfigNode : public Object {
public:
/*************** Models ***************/
/*! \brief The path to the model directory. */
String model;
/*! \brief The path or identifier to the model library. */
String model_lib;
/*! \brief The path to the additional models' directories. */
Array<String> additional_models;
/*! \brief The path to the additional models' libraries. */
Array<String> additional_model_libs;
/*************** KV cache config and engine capacities ***************/
/*!
* \brief The engine mode in MLC LLM.
* \sa EngineMode
*/
EngineMode mode = EngineMode::kLocal;
/*!
* \brief A number in (0, 1) denoting the fraction of GPU memory used by the server in total.
* It is used to infer to maximum possible KV cache capacity.
* When it is unspecified, it defaults to 0.85.
* Under mode "local" or "interactive", the actual memory usage may be
* significantly smaller than this number. Under mode "server", the actual
* memory usage may be slightly larger than this number.
*/
float gpu_memory_utilization = 0.85;
/*! \brief The number of consecutive tokens handled in each page in paged KV cache. */
int kv_cache_page_size = 16;
/*!
* \brief The maximum number of sequences that are allowed to be
* processed by the KV cache at any time.
*/
int max_num_sequence = 4;
/*! \brief The maximum length allowed for a single sequence in the engine. */
int64_t max_total_sequence_length = 4096;
/*!
* \brief The maximum total number of tokens whose KV data are allowed
* to exist in the KV cache at any time.
*/
int64_t max_single_sequence_length = 4096;
/*! \brief The maximum total sequence length in a prefill. */
int64_t prefill_chunk_size = 1024;
/*! \brief The maximum history size for RNN state. KV cache does not need this. */
int max_history_size = 0;
/*************** Prefix cache ***************/
/*! \brief The prefix cache mode. */
PrefixCacheMode prefix_cache_mode = PrefixCacheMode::kRadix;
/*! \brief The maximum number of recycling sequences in prefix cache, default as max_num_sequence.
* And set 0 to disable prefix cache, set -1 to have infinite capacity prefix cache. */
int prefix_cache_max_num_recycling_seqs = -1;
/*************** Speculative decoding ***************/
/*! \brief The speculative mode. */
SpeculativeMode speculative_mode = SpeculativeMode::kDisable;
/*!
* \brief The number of tokens to generate in speculative proposal (draft).
* Being 0 means to enable adaptive speculative mode, where the draft length
* will be automatically adjusted based on engine state.
*/
int spec_draft_length = 0;
/*! \brief The number of tokens to generate in speculative tree decoding */
int spec_tree_width = 1;
/*************** Prefill mode ***************/
/*! \brief The prefill mode. */
PrefillMode prefill_mode = PrefillMode::kHybrid;
/*************** Debug ***************/
bool verbose = false;
String AsJSONString() const;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<EngineConfigNode>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.EngineConfig", EngineConfigNode, Object);
};
class EngineConfig : public ObjectRef {
public:
/*! \brief Create EngineConfig from JSON object and inferred config. */
static EngineConfig FromJSONAndInferredConfig(const tvm::ffi::json::Object& json,
const InferrableEngineConfig& inferred_config);
/*!
* \brief Get all the models and model libs from the JSON string for engine initialization.
* \return The parsed models/model libs from config or error message.
*/
static Result<std::vector<std::pair<std::string, std::string>>>
GetModelsAndModelLibsFromJSONString(const std::string& json_str);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(EngineConfig, ObjectRef, EngineConfigNode);
};
/*! \brief A subset of engine config that is inferrable. */
struct InferrableEngineConfig {
std::optional<int64_t> max_num_sequence;
std::optional<int64_t> max_total_sequence_length;
std::optional<int64_t> max_single_sequence_length;
std::optional<int64_t> prefill_chunk_size;
std::optional<int64_t> max_history_size;
/*! \brief Infer the config for KV cache from a given initial config. */
static Result<InferrableEngineConfig> InferForKVCache(
EngineMode mode, Device device, double gpu_memory_utilization,
const std::vector<tvm::ffi::json::Object>& model_configs,
const std::vector<ModelMetadata>& model_metadata, InferrableEngineConfig init_config,
bool verbose);
/*! \brief Infer the config for RNN state from a given initial config. */
static Result<InferrableEngineConfig> InferForRNNState(
EngineMode mode, Device device, double gpu_memory_utilization,
const std::vector<tvm::ffi::json::Object>& model_configs,
const std::vector<ModelMetadata>& model_metadata, InferrableEngineConfig init_config,
bool verbose);
};
/****************** Config utils ******************/
/*! \brief Check if the models use KV cache or RNN state. */
Result<bool> ModelsUseKVCache(const std::vector<tvm::ffi::json::Object>& model_configs);
inline std::string EngineModeToString(EngineMode mode) {
if (mode == EngineMode::kLocal) {
return "local";
} else if (mode == EngineMode::kInteractive) {
return "interactive";
} else if (mode == EngineMode::kServer) {
return "server";
} else {
LOG(FATAL) << "Invalid engine mode: " << static_cast<int>(mode);
throw;
}
}
inline EngineMode EngineModeFromString(const std::string& mode) {
if (mode == "local") {
return EngineMode::kLocal;
} else if (mode == "interactive") {
return EngineMode::kInteractive;
} else if (mode == "server") {
return EngineMode::kServer;
} else {
LOG(FATAL) << "Invalid engine mode string: " << mode;
throw;
}
}
inline std::string PrefixCacheModeToString(PrefixCacheMode prefix_cache_mode) {
if (prefix_cache_mode == PrefixCacheMode::kDisable) {
return "disable";
} else if (prefix_cache_mode == PrefixCacheMode::kRadix) {
return "radix";
} else {
LOG(FATAL) << "Invalid prefix cache mode: " << static_cast<int>(prefix_cache_mode);
}
}
inline PrefixCacheMode PrefixCacheModeFromString(const std::string& prefix_cache_mode) {
if (prefix_cache_mode == "disable") {
return PrefixCacheMode::kDisable;
} else if (prefix_cache_mode == "radix") {
return PrefixCacheMode::kRadix;
} else {
LOG(FATAL) << "Invalid prefix cache mode string: " << prefix_cache_mode;
throw;
}
}
inline std::string SpeculativeModeToString(SpeculativeMode speculative_mode) {
if (speculative_mode == SpeculativeMode::kDisable) {
return "disable";
} else if (speculative_mode == SpeculativeMode::kSmallDraft) {
return "small_draft";
} else if (speculative_mode == SpeculativeMode::kEagle) {
return "eagle";
} else if (speculative_mode == SpeculativeMode::kMedusa) {
return "medusa";
} else {
LOG(FATAL) << "Invalid speculative mode: " << static_cast<int>(speculative_mode);
}
}
inline SpeculativeMode SpeculativeModeFromString(const std::string& speculative_mode) {
if (speculative_mode == "disable") {
return SpeculativeMode::kDisable;
} else if (speculative_mode == "small_draft") {
return SpeculativeMode::kSmallDraft;
} else if (speculative_mode == "eagle") {
return SpeculativeMode::kEagle;
} else if (speculative_mode == "medusa") {
return SpeculativeMode::kMedusa;
} else {
LOG(FATAL) << "Invalid speculative mode string: " << speculative_mode;
throw;
}
}
inline std::string PrefillModeToString(PrefillMode prefill_mode) {
if (prefill_mode == PrefillMode::kChunked) {
return "chunked";
} else if (prefill_mode == PrefillMode::kHybrid) {
return "hybrid";
} else {
LOG(FATAL) << "Invalid prefill mode: " << static_cast<int>(prefill_mode);
}
}
inline PrefillMode PrefillModeFromString(const std::string& prefill_mode) {
if (prefill_mode == "chunked") {
return PrefillMode::kChunked;
} else if (prefill_mode == "hybrid") {
return PrefillMode::kHybrid;
} else {
LOG(FATAL) << "Invalid prefill mode string: " << prefill_mode;
throw;
}
}
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_CONFIG_H_
+266
View File
@@ -0,0 +1,266 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/data.cc
*/
#include "data.h"
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include "model.h"
namespace mlc {
namespace llm {
namespace serve {
TVM_FFI_STATIC_INIT_BLOCK() {
DataNode::RegisterReflection();
TextDataNode::RegisterReflection();
TokenDataNode::RegisterReflection();
ImageDataNode::RegisterReflection();
RequestStreamOutputObj::RegisterReflection();
}
/****************** Data ******************/
std::pair<Array<Data>, Array<Data>> SplitData(const Array<Data>& original_data, int total_length,
int split_pos) {
TVM_FFI_ICHECK_GE(split_pos, 0);
TVM_FFI_ICHECK_GE(total_length, split_pos)
<< "Cannot truncate when the current length is already less than the target length";
std::vector<Data> lhs(original_data.begin(), original_data.end());
std::vector<Data> rhs;
while (total_length > split_pos) {
TVM_FFI_ICHECK(!lhs.empty());
Data last_data = lhs.back();
int last_data_length = last_data->GetLength();
TVM_FFI_ICHECK_GE(total_length - last_data_length, 0);
if (total_length - last_data_length >= split_pos) {
// Pop the entire last data.
rhs.push_back(lhs.back());
lhs.pop_back();
total_length -= last_data_length;
continue;
}
// Partially truncate the last data.
const auto* token_data = last_data.as<TokenDataNode>();
TVM_FFI_ICHECK(token_data != nullptr) << "Only TokenData supports partial truncation.";
int length_to_truncate = total_length - split_pos;
TVM_FFI_ICHECK_GT(length_to_truncate, 0);
TVM_FFI_ICHECK_LT(length_to_truncate, last_data_length);
TokenData lhs_token_data(
Shape{token_data->token_ids.begin(), token_data->token_ids.end() - length_to_truncate});
TokenData rhs_token_data(
Shape{token_data->token_ids.end() - length_to_truncate, token_data->token_ids.end()});
TVM_FFI_ICHECK_EQ(total_length - last_data_length + lhs_token_data->GetLength(), split_pos);
lhs.pop_back();
lhs.push_back(lhs_token_data);
rhs.push_back(rhs_token_data);
std::reverse(rhs.begin(), rhs.end());
total_length = split_pos;
}
return {lhs, rhs};
}
/****************** TextData ******************/
TextData::TextData(String text) {
ObjectPtr<TextDataNode> n = tvm::ffi::make_object<TextDataNode>();
n->text = std::move(text);
data_ = std::move(n);
}
int TextDataNode::GetLength() const {
LOG(FATAL) << "\"GetLength\" for TextData is not supported. "
"Please tokenize the text and construct a TokenData object.";
}
ObjectRef TextDataNode::GetEmbedding(Model model, ObjectRef* dst, int offset) const {
LOG(FATAL) << "\"GetEmbedding\" for TextData is not supported. "
"Please tokenize the text and construct a TokenData object.";
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("mlc.serve.TextData", [](String text) { return TextData(std::move(text)); })
.def("mlc.serve.TextDataGetTextString", [](TextData data) { return data->text; });
}
/****************** TokenData ******************/
TokenData::TokenData(Shape token_ids) {
ObjectPtr<TokenDataNode> n = tvm::ffi::make_object<TokenDataNode>();
n->token_ids = std::move(token_ids);
data_ = std::move(n);
}
TokenData::TokenData(std::vector<int32_t> token_ids) {
ObjectPtr<TokenDataNode> n = tvm::ffi::make_object<TokenDataNode>();
n->token_ids = Shape(token_ids.begin(), token_ids.end());
data_ = std::move(n);
}
int TokenDataNode::GetLength() const { return token_ids.size(); }
ObjectRef TokenDataNode::GetEmbedding(Model model, ObjectRef* dst, int offset) const {
return model->TokenEmbed(token_ids, dst, offset);
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def_packed("mlc.serve.TokenData",
[](ffi::PackedArgs args, ffi::Any* rv) {
std::vector<int32_t> token_ids;
token_ids.reserve(args.size());
for (int i = 0; i < args.size(); i++) {
token_ids.push_back(args[i].cast<int32_t>());
}
*rv = TokenData(std::move(token_ids));
})
.def("mlc.serve.TokenDataGetTokenIds", [](TokenData data) { return data->token_ids; });
}
/****************** ImageData ******************/
ImageData::ImageData(Tensor image, int embed_size) {
ObjectPtr<ImageDataNode> n = tvm::ffi::make_object<ImageDataNode>();
n->image = std::move(image);
n->embed_size = embed_size;
data_ = std::move(n);
}
int ImageDataNode::GetLength() const { return embed_size; }
ObjectRef ImageDataNode::GetEmbedding(Model model, ObjectRef* dst, int offset) const {
return model->ImageEmbed(image, dst, offset);
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("mlc.serve.ImageData",
[](Tensor image, int embed_size) { return ImageData(std::move(image), embed_size); })
.def("mlc.serve.ImageDataGetImage", [](ImageData data) { return data->image; });
}
/****************** SampleResult ******************/
/*! \brief Convert a single token with probability to JSON string. */
inline void TokenToLogProbJSON(const Tokenizer& tokenizer, const TokenProbPair& token_prob,
std::ostringstream* os) {
const std::string& token = tokenizer->PostProcessedTokenTable()[token_prob.first];
(*os) << "\"token\": \"";
for (char ch : token) {
if (ch >= 33 && ch <= 126) {
// The character is in ASCII visible range.
// Handle escape characters in JSON.
if (ch == '"') {
(*os) << "\\\"";
} else if (ch == '\\') {
(*os) << "\\\\";
} else {
(*os) << ch;
}
}
}
(*os) << "\", ";
(*os) << "\"logprob\": " << std::log(std::max(token_prob.second, 1e-10f)) << ", ";
(*os) << "\"bytes\": [";
int token_len = token.size();
for (int pos = 0; pos < token_len; ++pos) {
(*os) << static_cast<int>(static_cast<unsigned char>(token[pos]));
if (pos != token_len - 1) {
(*os) << ", ";
}
}
(*os) << "]";
}
int32_t SampleResult::GetTokenId() const { return this->sampled_token_id.first; }
std::string SampleResult::GetLogProbJSON(const Tokenizer& tokenizer, bool logprob) const {
TVM_FFI_ICHECK(top_prob_tokens.empty() || logprob);
if (!logprob) {
// Logprob is not needed.
return "";
}
std::ostringstream os;
os << "{";
// - Convert the sampled token to JSON.
TokenToLogProbJSON(tokenizer, sampled_token_id, &os);
// - Convert the tokens with top probabilities.
os << ", \"top_logprobs\": [";
int num_top = top_prob_tokens.size();
for (int i = 0; i < num_top; ++i) {
os << "{";
TokenToLogProbJSON(tokenizer, top_prob_tokens[i], &os);
os << "}";
if (i != num_top - 1) {
os << ", ";
}
}
os << "]}";
return os.str();
}
/****************** RequestStreamOutput ******************/
RequestStreamOutput::RequestStreamOutput(
String request_id, std::vector<std::vector<int64_t>> group_delta_token_ids,
std::optional<std::vector<std::vector<String>>> group_delta_logprob_json_strs,
std::vector<Optional<String>> group_finish_reason,
std::vector<String> group_extra_prefix_string) {
ObjectPtr<RequestStreamOutputObj> n = tvm::ffi::make_object<RequestStreamOutputObj>();
n->request_id = std::move(request_id);
n->group_delta_token_ids = std::move(group_delta_token_ids);
n->group_delta_logprob_json_strs = std::move(group_delta_logprob_json_strs);
n->group_finish_reason = std::move(group_finish_reason);
n->group_extra_prefix_string = std::move(group_extra_prefix_string);
data_ = std::move(n);
}
RequestStreamOutput RequestStreamOutput::Usage(String request_id,
String request_final_usage_json_str) {
ObjectPtr<RequestStreamOutputObj> n = tvm::ffi::make_object<RequestStreamOutputObj>();
n->request_id = std::move(request_id);
n->request_final_usage_json_str = std::move(request_final_usage_json_str);
return RequestStreamOutput(n);
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("mlc.serve.RequestStreamOutputUnpack", [](RequestStreamOutput output) {
TVM_FFI_ICHECK(!output->unpacked)
<< "One RequestStreamOutput can be unpacked for at most once.";
std::vector<Shape> group_delta_token_ids;
std::vector<Array<String>> group_delta_logprob_json_strs;
group_delta_token_ids.reserve(output->group_delta_token_ids.size());
if (output->group_delta_logprob_json_strs.has_value()) {
group_delta_logprob_json_strs.reserve(output->group_delta_token_ids.size());
}
for (int i = 0; i < static_cast<int>(output->group_delta_token_ids.size()); ++i) {
group_delta_token_ids.push_back(output->group_delta_token_ids[i]);
if (output->group_delta_logprob_json_strs.has_value()) {
group_delta_logprob_json_strs.push_back(output->group_delta_logprob_json_strs.value()[i]);
}
}
Array<Any> ret = {output->request_id,
Array<Shape>(std::move(group_delta_token_ids)),
output->group_delta_logprob_json_strs.has_value()
? Array<Array<String>>(std::move(group_delta_logprob_json_strs))
: Optional<Array<Array<String>>>(),
Array<Optional<String>>(output->group_finish_reason),
output->request_final_usage_json_str,
Array<String>(output->group_extra_prefix_string)};
output->unpacked = true;
return ret;
});
}
} // namespace serve
} // namespace llm
} // namespace mlc
+257
View File
@@ -0,0 +1,257 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/data.h
*/
#ifndef MLC_LLM_SERVE_DATA_H_
#define MLC_LLM_SERVE_DATA_H_
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/optional.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
#include <tvm/runtime/tensor.h>
#include <atomic>
#include <optional>
#include "../tokenizers/tokenizers.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectPtr;
using tvm::ffi::ObjectRef;
using tvm::ffi::Optional;
using tvm::ffi::Shape;
class Model;
/****************** DataNode ******************/
/*! \brief The base class of multi-modality data (text, tokens, embedding, etc). */
class DataNode : public Object {
public:
/*! \brief Get the length (equivalent number of tokens) of the data. */
virtual int GetLength() const = 0;
/*!
* \brief Compute the embedding of this data with regard to the input model.
* When the input destination pointer is not nullptr, it in-place writes the
* embedding into the input destination array at the given offset.
* Otherwise, the embeddings will be directly returned back.
* \param model The model to take embeddings from.
* \param dst The destination array of the embedding lookup.
* \param offset The token offset where the computed embeddings will be written
* into the destination array.
* \return The updated destination embedding array or the computed embeddings.
* \note When `dst` is nullptr, we require `offset` to be 0.
*/
virtual ObjectRef GetEmbedding(Model model, ObjectRef* dst = nullptr, int offset = 0) const = 0;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<DataNode>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const uint32_t _type_child_slots = 3;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.Data", DataNode, Object);
};
class Data : public ObjectRef {
public:
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(Data, ObjectRef, DataNode);
};
/*! \brief Split the given data array into two arrays at the "split_pos" position. */
std::pair<Array<Data>, Array<Data>> SplitData(const Array<Data>& original_data, int total_length,
int split_pos);
/****************** TextDataNode ******************/
/*! \brief The class of text data, containing a text string. */
class TextDataNode : public DataNode {
public:
/*! \brief The text string. */
tvm::ffi::String text;
int GetLength() const final;
ObjectRef GetEmbedding(Model model, ObjectRef* dst = nullptr, int offset = 0) const final;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<TextDataNode>();
}
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("mlc.serve.TextData", TextDataNode, DataNode);
};
class TextData : public Data {
public:
explicit TextData(String text);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TextData, Data, TextDataNode);
};
/****************** TokenDataNode ******************/
/*! \brief The class of token data, containing a list of token ids. */
class TokenDataNode : public DataNode {
public:
/*! \brief The token ids. */
Shape token_ids;
int GetLength() const final;
ObjectRef GetEmbedding(Model model, ObjectRef* dst = nullptr, int offset = 0) const final;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<TokenDataNode>();
}
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("mlc.serve.TokenData", TokenDataNode, DataNode);
};
class TokenData : public Data {
public:
explicit TokenData(Shape token_ids);
explicit TokenData(std::vector<int32_t> token_ids);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TokenData, Data, TokenDataNode);
};
/****************** ImageDataNode ******************/
/*! \brief The class of image data, containing a 3D array of pixel values. */
class ImageDataNode : public DataNode {
public:
/*! \brief The pixel values. */
Tensor image;
int embed_size;
int GetLength() const final;
ObjectRef GetEmbedding(Model model, ObjectRef* dst = nullptr, int offset = 0) const final;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<ImageDataNode>();
}
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("mlc.serve.ImageData", ImageDataNode, DataNode);
};
class ImageData : public Data {
public:
explicit ImageData(Tensor image, int embed_size);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(ImageData, Data, ImageDataNode);
};
/****************** SampleResult ******************/
// The pair of a token id and its probability in sampling.
using TokenProbPair = std::pair<int32_t, float>;
/*!
* \brief The class of sampler's sampling result.
* It's not a TVM object since it will not be used directly on Python side.
*/
struct SampleResult {
/*! \brief The token id and probability of the sampled token. */
TokenProbPair sampled_token_id;
/*! \brief The token id and probability of the tokens with top probabilities. */
std::vector<TokenProbPair> top_prob_tokens;
/*! \brief Get the sampled token id. */
int32_t GetTokenId() const;
/*!
* \brief Get the logprob JSON string of this token with regard
* to OpenAI API at https://platform.openai.com/docs/api-reference/chat/object.
* \param tokenizer The tokenizer for token table lookup.
* \param logprob A boolean indicating if need to return log probability.
* \return A JSON string that conforms to the logprob spec in OpenAI API.
*/
std::string GetLogProbJSON(const Tokenizer& tokenizer, bool logprob) const;
};
/****************** RequestStreamOutput ******************/
/*!
* \brief The generated delta request output that is streamed back
* through callback stream function.
*
* \note: This output object corresponds to parallel generated outputs when n != 1.
*
* For example, if n=2, then group_delta_token_ids[0] matches to the output stream 0
* and group_delta_token_ids[1] matches to the output stream 1
*/
class RequestStreamOutputObj : public Object {
public:
/*! \brief The id of the request that the function is invoked for. */
String request_id;
/*!
* \brief The new generated token ids since the last callback invocation
* for the input request.
*/
std::vector<std::vector<int64_t>> group_delta_token_ids;
/*! \brief The logprobs JSON strings of the new generated tokens since last invocation. */
std::optional<std::vector<std::vector<String>>> group_delta_logprob_json_strs;
/*!
* \brief The finish reason of the request when it is finished,
* of None if the request has not finished yet.
*/
std::vector<Optional<String>> group_finish_reason;
/*!
* \brief The usage field of the response, this is global to all streams.
*/
Optional<String> request_final_usage_json_str;
/*!
* \brief The extra prefix string of all requests.
*/
std::vector<String> group_extra_prefix_string;
std::atomic<bool> unpacked = false;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<RequestStreamOutputObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.RequestStreamOutput", RequestStreamOutputObj, Object);
};
/*!
* \brief Managed reference to RequestStreamOutputObj.
* \sa RequestStreamOutputObj
*/
class RequestStreamOutput : public ObjectRef {
public:
explicit RequestStreamOutput(
String request_id, std::vector<std::vector<int64_t>> group_delta_token_ids,
std::optional<std::vector<std::vector<String>>> group_delta_logprob_json_strs,
std::vector<Optional<String>> group_finish_reason,
std::vector<String> group_extra_prefix_string);
static RequestStreamOutput Usage(String request_id, String request_final_usage_json_str);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(RequestStreamOutput, ObjectRef,
RequestStreamOutputObj);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_DATA_H_
@@ -0,0 +1,77 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/draft_token_workspace_manager.cc
*/
#include "draft_token_workspace_manager.h"
#include "model.h"
namespace mlc {
namespace llm {
namespace serve {
TVM_FFI_STATIC_INIT_BLOCK() { DraftTokenWorkspaceManagerObj::RegisterReflection(); }
DraftTokenWorkspaceManagerObj::DraftTokenWorkspaceManagerObj(int max_num_tokens, int vocab_size,
int hidden_size,
DLDataType hidden_states_dtype,
DLDevice device,
const FunctionTable& ft)
: max_num_tokens_(max_num_tokens),
vocab_size_(vocab_size),
hidden_size_(hidden_size),
hidden_states_dtype_(hidden_states_dtype),
device_(device),
ft_(ft) {
free_slots_.resize(max_num_tokens);
std::iota(free_slots_.begin(), free_slots_.end(), 0);
}
void DraftTokenWorkspaceManagerObj::AllocSlots(int num_slots, std::vector<int>* result) {
TVM_FFI_ICHECK_LE(num_slots, free_slots_.size());
result->assign(free_slots_.rbegin(), free_slots_.rbegin() + num_slots);
free_slots_.resize(free_slots_.size() - num_slots);
for (int slot : (*result)) {
ref_count_[slot] = 1;
}
}
void DraftTokenWorkspaceManagerObj::AllocSlots(int num_slots,
const std::vector<int>& initial_ref_count,
std::vector<int>* result) {
TVM_FFI_ICHECK_LE(num_slots, free_slots_.size());
TVM_FFI_ICHECK_EQ(num_slots, initial_ref_count.size());
result->assign(free_slots_.rbegin(), free_slots_.rbegin() + num_slots);
free_slots_.resize(free_slots_.size() - num_slots);
for (int i = 0; i < num_slots; ++i) {
int slot = (*result)[i];
TVM_FFI_ICHECK(initial_ref_count[i] > 0);
ref_count_[slot] = initial_ref_count[i];
}
}
void DraftTokenWorkspaceManagerObj::FreeSlots(const std::vector<int>& slots) {
for (int slot : slots) {
if (--ref_count_.at(slot) == 0) {
free_slots_.push_back(slot);
ref_count_.erase(slot);
}
}
}
void DraftTokenWorkspaceManagerObj::AllocWorkspace(ModelWorkspace* workspace,
bool require_hidden_states) {
workspace->draft_probs =
Tensor::Empty({max_num_tokens_, vocab_size_}, DLDataType{kDLFloat, 32, 1}, device_);
workspace->draft_probs_storage =
Tensor::Empty({max_num_tokens_, vocab_size_}, DLDataType{kDLFloat, 32, 1}, device_);
if (require_hidden_states) {
workspace->draft_hidden_states_storage = ft_.Empty(
{max_num_tokens_, hidden_size_}, hidden_states_dtype_, device_, /*worker0_only=*/false);
}
}
} // namespace serve
} // namespace llm
} // namespace mlc
+117
View File
@@ -0,0 +1,117 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/draft_token_workspace_manager.h
*/
#ifndef MLC_LLM_SERVE_DRAFT_TOKEN_WORKSPACE_MANAGER_H_
#define MLC_LLM_SERVE_DRAFT_TOKEN_WORKSPACE_MANAGER_H_
#include <tvm/ffi/reflection/registry.h>
#include <tvm/runtime/device_api.h>
#include <numeric>
#include <optional>
#include <vector>
#include "data.h"
#include "function_table.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::Device;
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
struct ModelWorkspace;
/*!
* \brief Managing the workspace for draft token generation.
*
* The workspace is used to store the associated states for each draft token, including the
* probability distribution of the draft token, the hidden states, etc. The workspace manager
* maintains a pool of slots for the draft tokens to store the states.
*/
class DraftTokenWorkspaceManagerObj : public Object {
public:
/*!
* \brief Constructor
* \param max_num_tokens The maximum number of draft tokens that can be stored in the workspace.
* \param vocab_size The size of the vocabulary.
* \param hidden_size The size of the hidden states.
* \param hidden_states_dtype The data type of the hidden states.
* \param device The device running the model.
* \param ft The function table.
*/
DraftTokenWorkspaceManagerObj(int max_num_tokens, int vocab_size, int hidden_size,
DLDataType hidden_states_dtype, DLDevice device,
const FunctionTable& ft);
/*!
* \brief Allocate the workspace for draft tokens and update `ModelWorkspace` data structure.
* \param workspace The object to stored the allocated draft token workspace.
* \param require_hidden_states Whether to allocate workspace for the hidden states.
*/
void AllocWorkspace(ModelWorkspace* workspace, bool require_hidden_states);
/*!
* \brief Allocate slots for the draft tokens.
* \param num_slots The number of slots to allocate.
* \param result The vector to store the allocated slots.
*/
void AllocSlots(int num_slots, std::vector<int>* result);
/*!
* \brief Allocate slots for the draft tokens.
* \param num_slots The number of slots to allocate.
* \param initial_ref_count The initial reference count for each slot.
* \param result The vector to store the allocated slots.
*/
void AllocSlots(int num_slots, const std::vector<int>& initial_ref_count,
std::vector<int>* result);
/*!
* \brief Free the slots.
* \param slots The slots to free.
*/
void FreeSlots(const std::vector<int>& slots);
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<DraftTokenWorkspaceManagerObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("mlc.serve.DraftTokenWorkspaceManager",
DraftTokenWorkspaceManagerObj, Object);
private:
std::vector<int> free_slots_;
int max_num_tokens_;
int vocab_size_;
int hidden_size_;
DLDataType hidden_states_dtype_;
DLDevice device_;
const FunctionTable& ft_;
std::unordered_map<int, int> ref_count_;
};
class DraftTokenWorkspaceManager : public ObjectRef {
public:
DraftTokenWorkspaceManager(int max_num_tokens, int vocab_size, int hidden_size,
DLDataType hidden_states_dtype, DLDevice device,
const FunctionTable& ft) {
data_ = tvm::ffi::make_object<DraftTokenWorkspaceManagerObj>(
max_num_tokens, vocab_size, hidden_size, hidden_states_dtype, device, ft);
}
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(DraftTokenWorkspaceManager, ObjectRef,
DraftTokenWorkspaceManagerObj);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_DRAFT_TOKEN_WORKSPACE_MANAGER_H_
+1102
View File
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine.h
* \brief The header of serving engine in MLC LLM.
*/
#ifndef MLC_LLM_SERVE_ENGINE_H_
#define MLC_LLM_SERVE_ENGINE_H_
#include "data.h"
#include "engine_state.h"
#include "event_trace_recorder.h"
#include "request.h"
#include "request_state.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
class Engine;
/*!
* \brief The output of engine creation, including the created engine and
* the default generation config for requests.
*/
struct EngineCreationOutput {
std::unique_ptr<Engine> reloaded_engine;
EngineConfig completed_engine_config;
GenerationConfig default_generation_cfg;
};
/*!
* \brief The engine interface for request serving in MLC LLM.
* The engine can run one or multiple LLM models internally for
* text generation. Usually, when there are multiple models,
* speculative inference will be activated, where the first model
* (index 0) is the main "large model" that has better generation
* quality, and all other models are "small" models that used for
* speculation.
* The engine receives requests from the "AddRequest" method. For
* an given request, the engine will keep generating new tokens for
* the request until finish (under certain criterion). After finish,
* the engine will return the generation result through the callback
* function provided by the request.
* \note For now only one model run in the engine is supported.
* Multiple model support such as speculative inference will
* be followed soon in the future.
*
* The public interface of Engine has the following three categories:
* - engine management,
* - high-level request management,
* - engine "step" action.
*/
class Engine {
public:
/********************** Engine Management **********************/
virtual ~Engine() = default;
/*!
* \brief Create an engine in unique pointer.
* \param engine_config_json_str The serialized JSON string of the engine config.
* \param device The device where the run models.
* \param request_stream_callback The request stream callback function to.
* \param trace_recorder Event trace recorder for requests.
* \return The created Engine in pointer, and the default generation config.
*/
static Result<EngineCreationOutput> Create(const std::string& engine_config_json_str,
Device device,
FRequestStreamCallback request_stream_callback,
Optional<EventTraceRecorder> trace_recorder);
/*! \brief Reset the engine, clean up all running data and metrics. */
virtual void Reset() = 0;
/*! \brief Check if the engine has no request to process. */
virtual bool Empty() = 0;
/*! \brief Get the request stream callback function of the engine. */
virtual FRequestStreamCallback GetRequestStreamCallback() = 0;
/*! \brief Set the request stream callback function of the engine. */
virtual void SetRequestStreamCallback(FRequestStreamCallback request_stream_callback) = 0;
/***************** High-level Request Management *****************/
/*! \brief Add a new request to the engine. */
virtual void AddRequest(Request request) = 0;
/*! \brief Abort the input request (specified by id string) from engine. */
virtual void AbortRequest(const String& request_id) = 0;
/*! \brief Abort all requests from the engine. */
virtual void AbortAllRequests() = 0;
/*********************** Engine Action ***********************/
/*!
* \brief The main function that the engine takes a step of action.
* At each step, the engine may decide to
* - run prefill for one (or more) requests,
* - run one-step decode for the all existing requests
* ...
* In the end of certain actions (e.g., decode), the engine will
* check if any request has finished, and will return the
* generation results for those finished requests.
*/
virtual void Step() = 0;
/************** Debug/Profile **************/
/*! \brief Internal engine metrics. */
virtual String JSONMetrics() = 0;
/*! \brief Call the given global function on all workers. Only for debug purpose. */
virtual void DebugCallFuncOnAllAllWorker(const String& func_name, Optional<String> func_args) = 0;
};
void AbortRequestImpl(EngineState estate, const Array<Model>& models, const String& request_id,
String finish_reason = "abort");
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_ENGINE_H_
+16
View File
@@ -0,0 +1,16 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/action.cc
*/
#include "action.h"
namespace mlc {
namespace llm {
namespace serve {
TVM_FFI_STATIC_INIT_BLOCK() { EngineActionObj::RegisterReflection(); }
} // namespace serve
} // namespace llm
} // namespace mlc
+264
View File
@@ -0,0 +1,264 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/action.h
* \brief The abstraction of actions (e.g., prefill/decode) that an
* Engine can take at each time step.
*/
#ifndef MLC_LLM_SERVE_ENGINE_ACTIONS_ACTION_H_
#define MLC_LLM_SERVE_ENGINE_ACTIONS_ACTION_H_
#include "../config.h"
#include "../draft_token_workspace_manager.h"
#include "../engine.h"
#include "../engine_state.h"
#include "../event_trace_recorder.h"
#include "../model.h"
#include "../sampler/sampler.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
/*!
* \brief The abstraction of actions that an Engine can take at each time step.
* The only core interface of an action is the `Step` function.
* At high level, the Step function takes the current engine state
* as input, invokes model functions (such as batched-prefill or
* batched-decode), run sampler to sample new tokens, and update
* the engine state.
*/
class EngineActionObj : public Object {
public:
/*!
* \brief The behavior of the engine action in a single step.
* \param estate The engine state to be analyzed and updated.
* \return The processed requests in this step.
*/
virtual Array<Request> Step(EngineState estate) = 0;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<EngineActionObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.EngineAction", EngineActionObj, Object);
};
/*!
* \brief Managed reference of EngineActionObj.
* It declares the full list of supported actions.
* \sa EngineActionObj
*/
class EngineAction : public ObjectRef {
public:
/*!
* \brief Create the action that prefills requests in the `waiting_queue`
* of the engine state.
* \param models The models to run prefill in.
* \param logit_processor The logit processor.
* \param sampler The sampler to sample new tokens.
* \param model_workspaces The workspace of each model.
* \param engine_config The engine config.
* \param model_configs The config of each model.
* \param trace_recorder The event trace recorder for requests.
* \return The created action object.
*/
static EngineAction NewRequestPrefill(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Create the action that prefills requests in the `waiting_queue`
* of the engine state.
* \param models The models to run prefill in.
* \param logit_processor The logit processor.
* \param sampler The sampler to sample new tokens.
* \param model_workspaces The workspace of each model.
* \param draft_token_workspace_manager The draft token workspace manager.
* \param engine_config The engine config.
* \param model_configs The config of each model.
* \param trace_recorder The event trace recorder for requests.
* \return The created action object.
*/
static EngineAction EagleNewRequestPrefill(
Array<Model> models, LogitProcessor logit_processor, Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Create the action that runs one-step decode for requests in the
* `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
* \note The BatchDecode action **does not** take effect for speculative
* decoding scenarios where there are multiple models. For speculative
* decoding in the future, we will use other specific actions.
* \param models The model to run decode in. When there are multiple
* models, the `Step` function of the created action will not take effect.
* \param tokenizer The tokenizer of the engine.
* \param sampler The sampler to sample new tokens.
* \param engine_config The engine config.
* \param trace_recorder The event trace recorder for requests.
* \return The created action object.
*/
static EngineAction BatchDecode(Array<Model> models, Tokenizer tokenizer,
LogitProcessor logit_processor, Sampler sampler,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Create the action that runs one-step speculative draft proposal for
* requests in the `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
* \param models The model to run decode in. When there are multiple
* models, the `Step` function of the created action will not take effect.
* \param sampler The sampler to sample new tokens.
* \param model_workspaces The workspace of each model.
* \param draft_token_workspace_manager The draft token workspace manager.
* \param engine_config The engine config.
* \param trace_recorder The event trace recorder for requests.
* \return The created action object.
*/
static EngineAction BatchDraft(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler, std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Create the action that runs one-step speculative draft proposal for
* requests in the `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
* \param models The model to run decode in. When there are multiple
* models, the `Step` function of the created action will not take effect.
* \param sampler The sampler to sample new tokens.
* \param model_workspaces The workspace of each model.
* \param draft_token_workspace_manager The draft token workspace manager.
* \param engine_config The engine config.
* \param trace_recorder The event trace recorder for requests.
* \return The created action object.
*/
static EngineAction EagleBatchDraft(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler, std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Create the action that runs one-step speculative verification for requests in the
* `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
* \param models The model to run decode in. When there are multiple
* models, the `Step` function of the created action will not take effect.
* \param model_workspaces The workspace of each model.
* \param draft_token_workspace_manager The draft token workspace manager.
* \param sampler The sampler to sample new tokens.
* \param engine_config The engine config.
* \param trace_recorder The event trace recorder for requests.
* \return The created action object.
*/
static EngineAction BatchVerify(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler, std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Create the action that runs one-step speculative verification for requests in the
* `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
* \param models The model to run decode in. When there are multiple
* models, the `Step` function of the created action will not take effect.
* \param sampler The sampler to sample new tokens.
* \param model_workspaces The workspace of each model.
* \param draft_token_workspace_manager The draft token workspace manager.
* \param engine_config The engine config.
* \param trace_recorder The event trace recorder for requests.
* \return The created action object.
*/
static EngineAction EagleBatchVerify(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Create the action that executes the jump-forward decoding to predict the next tokens
* according to the grammar constraint. Does nothing for the requests without grammar. The
* predicted tokens will be fed to the next BatchDecode action. Retokenization may happen when
* the predicted string breaks the tokenization boundary.
* \param models The model to run decode in. When there are multiple
* models, the `Step` function of the created action will not take effect.
* \param tokenizer The tokenizer of the engine.
* \param trace_recorder The event trace recorder for requests.
* \return The created action object.
*/
static EngineAction BatchJumpForward(Array<Model> models, Tokenizer tokenizer,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Create the action that first makes a decision on whether to run speculative
* decoding or normal mode batch decode, and then runs the selected actions.
* \param spec_decode_actions The actions for speculative decoding.
* \param batch_decode_actions The actions for normal mode batch decoding.
* \param engine_config The engine config.
* \return The created action object
*/
static EngineAction AutoSpecDecode(std::vector<EngineAction> spec_decode_actions,
std::vector<EngineAction> batch_decode_actions,
EngineConfig engine_config);
/*!
* \brief Create the action that runs the disaggregation preparation for prefill.
* \param models The underlying models whose KV cache are to be updated.
* \param engine_config The engine config.
* \param model_configs The config of each model.
* \param trace_recorder The event trace recorder for requests.
* \param request_stream_callback The stream callback function to pass the prefill
* preparation result back, including the KV cache append metadata and the prefix
* matched length in the prefix cache.
* \return The created action object.
*/
static EngineAction DisaggPrepareReceive(Array<Model> models, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder,
FRequestStreamCallback request_stream_callback);
/*!
* \brief Create the action that runs the prefill and sends KV data to remote instance.
* \param models The underlying models whose KV cache are to be updated.
* \param model_workspaces The workspace of each model.
* \param engine_config The engine config.
* \param model_configs The config of each model.
* \param trace_recorder The event trace recorder for requests.
* \param request_stream_callback The stream callback function to pass the prefill
* preparation result back, including the KV cache append metadata and the prefix
* matched length in the prefix cache.
* \param device The device of the model for synchronization.
* \return The created action object.
*/
static EngineAction DisaggRemoteSend(Array<Model> models,
std::vector<ModelWorkspace> model_workspaces,
EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder,
FRequestStreamCallback request_stream_callback,
Device device);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(EngineAction, ObjectRef, EngineActionObj);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_ENGINE_ACTIONS_ACTION_H_
+452
View File
@@ -0,0 +1,452 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/action_commons.cc
*/
#include "action_commons.h"
#include <tvm/support/cuda/nvtx.h>
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
Array<EngineAction> CreateEngineActions(Array<Model> models, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
std::vector<ModelWorkspace> model_workspaces,
LogitProcessor logit_processor, Sampler sampler,
DraftTokenWorkspaceManager draft_token_workspace_manager,
Tokenizer tokenizer,
Optional<EventTraceRecorder> trace_recorder,
FRequestStreamCallback request_stream_callback,
Device device) {
Array<EngineAction> actions;
ModelMetadata model_metadata = models[0]->GetMetadata();
if (engine_config->speculative_mode != SpeculativeMode::kDisable) {
// Speculative decoding is only possible for more than one model.
TVM_FFI_ICHECK_GT(models.size(), 1U);
if (engine_config->speculative_mode == SpeculativeMode::kEagle) {
TVM_FFI_ICHECK_GT(engine_config->spec_draft_length, 0)
<< "The automatic spec decoding does not support Eagle mode as of now.";
actions = {EngineAction::EagleNewRequestPrefill(models, //
logit_processor, //
sampler, //
model_workspaces, //
draft_token_workspace_manager, //
engine_config, //
model_configs, //
trace_recorder),
EngineAction::EagleBatchDraft(models, logit_processor, sampler, model_workspaces,
draft_token_workspace_manager, engine_config,
trace_recorder),
EngineAction::EagleBatchVerify(models, logit_processor, sampler, model_workspaces,
draft_token_workspace_manager, engine_config,
trace_recorder)};
} else if (engine_config->speculative_mode == SpeculativeMode::kMedusa) {
TVM_FFI_ICHECK_GT(engine_config->spec_draft_length, 0)
<< "The automatic spec decoding does not support Eagle mode as of now.";
actions = {EngineAction::EagleNewRequestPrefill(models, //
logit_processor, //
sampler, //
model_workspaces, //
draft_token_workspace_manager, //
engine_config, //
model_configs, //
trace_recorder),
EngineAction::EagleBatchVerify(models, logit_processor, sampler, model_workspaces,
draft_token_workspace_manager, engine_config,
trace_recorder)};
} else if (engine_config->spec_draft_length > 0) {
// The "small draft" mode speculative decoding.
// If "engine_config->spec_draft_length" > 0, it means the draft length is
// configured to be a fixed value.
actions = {
EngineAction::NewRequestPrefill(models, //
logit_processor, //
sampler, //
model_workspaces, //
engine_config, //
model_configs, //
trace_recorder),
EngineAction::BatchDraft(models, logit_processor, sampler, model_workspaces,
draft_token_workspace_manager, engine_config, trace_recorder),
EngineAction::BatchVerify(models, logit_processor, sampler, model_workspaces,
draft_token_workspace_manager, engine_config, trace_recorder)};
} else {
// The "small draft" mode speculative decoding.
// "engine_config->spec_draft_length" being 0 means we want to enable
// automatic speculative decoding, which decides the spec decoding draft length
// automatically.
actions = {EngineAction::NewRequestPrefill(models, //
logit_processor, //
sampler, //
model_workspaces, //
engine_config, //
model_configs, //
trace_recorder),
EngineAction::AutoSpecDecode(
/*spec_decode_actions=*/{EngineAction::BatchDraft(
models, logit_processor, sampler,
model_workspaces, draft_token_workspace_manager,
engine_config, trace_recorder),
EngineAction::BatchVerify(
models, logit_processor, sampler,
model_workspaces, draft_token_workspace_manager,
engine_config, trace_recorder)},
/*batch_decode_actions=*/
{EngineAction::BatchDecode(models, tokenizer, logit_processor, sampler,
engine_config, trace_recorder)},
engine_config)};
}
} else if (model_metadata.disaggregation) {
actions = {EngineAction::NewRequestPrefill(models, //
logit_processor, //
sampler, //
model_workspaces, //
engine_config, //
model_configs, //
trace_recorder),
EngineAction::BatchDecode(models, tokenizer, logit_processor, sampler, engine_config,
trace_recorder)};
} else {
// The normal mode.
actions = {EngineAction::NewRequestPrefill(models, //
logit_processor, //
sampler, //
model_workspaces, //
engine_config, //
model_configs, //
trace_recorder),
EngineAction::BatchJumpForward(models, tokenizer, trace_recorder),
EngineAction::BatchDecode(models, tokenizer, logit_processor, sampler, engine_config,
trace_recorder)};
}
if (model_metadata.disaggregation) {
// Insert the disaggregation actions.
Array<EngineAction> disaggregation_actions = {
EngineAction::DisaggPrepareReceive(models, engine_config, model_configs, trace_recorder,
request_stream_callback),
EngineAction::DisaggRemoteSend(models, model_workspaces, engine_config, model_configs,
trace_recorder, request_stream_callback, device)};
actions.insert(actions.begin(), disaggregation_actions.begin(), disaggregation_actions.end());
}
return actions;
}
void RemoveRequestFromModel(EngineState estate, int64_t req_internal_id,
const Array<Model>& models) {
// Remove the request from all models (usually the KV cache).
for (Model model : models) {
model->RemoveSequence(req_internal_id);
}
}
/*!
* \brief Remove the given request state entry.
* \param estate The engine state to update after removal.
* \param models The models to remove the given request from.
* \param rsentry The request state entry to remove.
*/
void RemoveRequestStateEntry(EngineState estate, const Array<Model>& models,
RequestStateEntry rsentry,
Optional<DraftTokenWorkspaceManager> draft_token_workspace_manager) {
if (draft_token_workspace_manager.has_value()) {
std::vector<int> draft_token_slots;
for (const RequestModelState& mstate : rsentry->mstates) {
mstate->RemoveAllDraftTokens(&draft_token_slots);
draft_token_workspace_manager.value()->FreeSlots(draft_token_slots);
}
}
if (estate->prefix_cache->HasSequence(rsentry->mstates[0]->internal_id)) {
// If the sequence is stored in prefix cache, call prefix cache to remove.
if (!(rsentry->request->generation_cfg->debug_config.pinned_system_prompt)) {
// If the request is not pinned, recycle the request.
estate->prefix_cache->RecycleSequence(rsentry->mstates[0]->internal_id, /*lazy=*/true);
}
// If the request is pinned, do nothing over the prefix cache and KVCache.
} else {
// If the sequence is not stored in prefix cache, remove it directly.
RemoveRequestFromModel(estate, rsentry->mstates[0]->internal_id, models);
estate->id_manager.RecycleId(rsentry->mstates[0]->internal_id);
}
}
void ProcessFinishedRequestStateEntries(
const std::vector<RequestStateEntry>& finished_rsentries, EngineState estate,
const Array<Model>& models, int max_single_sequence_length,
Optional<DraftTokenWorkspaceManager> draft_token_workspace_manager,
Array<RequestStreamOutput>* callback_delta_outputs) {
NVTXScopedRange nvtx_scope("Process finished requests");
// - Remove the finished request state entries.
for (const RequestStateEntry& rsentry : finished_rsentries) {
// The finished entry must be a leaf.
TVM_FFI_ICHECK(rsentry->child_indices.empty());
// Mark the status of this entry as finished.
rsentry->status = RequestStateStatus::kFinished;
// Remove the request state entry from all the models.
RemoveRequestStateEntry(estate, models, rsentry, draft_token_workspace_manager);
RequestState rstate = estate->GetRequestState(rsentry->request);
int parent_idx = rsentry->parent_idx;
while (parent_idx != -1) {
bool all_children_finished = true;
for (int child_idx : rstate->entries[parent_idx]->child_indices) {
if (rstate->entries[child_idx]->status != RequestStateStatus::kFinished) {
all_children_finished = false;
break;
}
}
if (!all_children_finished) {
break;
}
// All the children of the parent request state entry have finished.
// So we mark the parent entry as finished.
rstate->entries[parent_idx]->status = RequestStateStatus::kFinished;
// Remove the request state entry from all the models.
RemoveRequestStateEntry(estate, models, rstate->entries[parent_idx],
draft_token_workspace_manager);
// Climb up to the parent.
parent_idx = rstate->entries[parent_idx]->parent_idx;
}
if (parent_idx == -1) {
// Remove from running queue and engine state.
auto it =
std::find(estate->running_queue.begin(), estate->running_queue.end(), rsentry->request);
TVM_FFI_ICHECK(it != estate->running_queue.end());
estate->running_queue.erase(it);
estate->request_states.erase(rsentry->request->id);
// Update engine metrics.
const RequestStateEntry& root_rsentry = rstate->entries[0];
auto trequest_finish = std::chrono::high_resolution_clock::now();
rstate->metrics.finish_time_point = trequest_finish;
estate->metrics.RequestFinishUpdate(rstate->metrics);
// always stream back usage in backend
callback_delta_outputs->push_back(RequestStreamOutput::Usage(
root_rsentry->request->id, rstate->metrics.AsUsageJSONStr(true)));
}
estate->running_rsentries_changed = true;
}
}
void ActionStepPostProcess(Array<Request> requests, EngineState estate, const Array<Model>& models,
const Tokenizer& tokenizer,
FRequestStreamCallback request_stream_callback,
int64_t max_single_sequence_length,
Optional<DraftTokenWorkspaceManager> draft_token_workspace_manager,
Optional<EventTraceRecorder> trace_recorder) {
NVTXScopedRange nvtx_scope("EngineAction postproc");
int num_requests = requests.size();
estate->postproc_workspace.finished_rsentries.clear();
estate->postproc_workspace.callback_delta_outputs.clear();
estate->postproc_workspace.finished_rsentries.reserve(num_requests);
estate->postproc_workspace.callback_delta_outputs.reserve(num_requests * 2);
// - Collect new generated tokens and finish reasons for requests.
for (int r = 0; r < num_requests; ++r) {
Request request = requests[r];
int n = request->generation_cfg->n;
RequestState rstate = estate->GetRequestState(requests[r]);
bool invoke_callback = false;
RequestStreamOutput stream_output = rstate->postproc_states.GetStreamOutput();
for (int i = 0; i < n; ++i) {
const RequestStateEntry& rsentry = n == 1 ? rstate->entries[0] : rstate->entries[i + 1];
rsentry->GetDeltaRequestReturn(tokenizer, max_single_sequence_length, &stream_output, i);
if (stream_output->group_finish_reason[i].has_value()) {
invoke_callback = true;
estate->postproc_workspace.finished_rsentries.push_back(rsentry);
}
if (!stream_output->group_delta_token_ids[i].empty() ||
!stream_output->group_extra_prefix_string[i].empty()) {
invoke_callback = true;
}
}
if (invoke_callback) {
stream_output->unpacked = false;
estate->postproc_workspace.callback_delta_outputs.push_back(std::move(stream_output));
}
// Update prefix cache and metrics.
for (const RequestStateEntry& rsentry : rstate->entries) {
std::vector<int32_t>& token_ids = rsentry->token_ids_for_prefix_cache_update;
token_ids.clear();
if (!rsentry->mstates[0]->prefilled_inputs.empty()) {
// Notify the prefix cache of the newly prefilled data.
for (const Data& data : rsentry->mstates[0]->prefilled_inputs) {
const TokenDataNode* token_data = data.as<TokenDataNode>();
if (token_data == nullptr) continue;
token_ids.insert(token_ids.end(), token_data->token_ids->data,
token_data->token_ids->data + token_data->token_ids.size());
// note that we are counting prefill tokens across all branches
rstate->metrics.prefill_tokens += data->GetLength();
}
rsentry->mstates[0]->prefilled_inputs.clear();
}
int64_t num_committed_tokens = rsentry->mstates[0]->committed_tokens.size();
if (rsentry->mstates[0]->cached_committed_tokens < num_committed_tokens - 1) {
// Notify the prefix cache of the newly decoded data, except the last token as it is not
// in KVCache yet.
for (int64_t& i = rsentry->mstates[0]->cached_committed_tokens;
i < num_committed_tokens - 1; ++i) {
token_ids.push_back(rsentry->mstates[0]->committed_tokens[i].sampled_token_id.first);
}
}
if (!token_ids.empty()) {
estate->prefix_cache->ExtendSequence(rsentry->mstates[0]->internal_id, token_ids);
}
}
// - For all disaggregation requests with "remote_send",
// if it does not appear in the waiting queue, it means the prefill has been finished.
// In this case, we mark the request as finished.
if (request->generation_cfg->debug_config.disagg_config.kind ==
DisaggRequestKind::kRemoteSend) {
auto it = std::find(estate->waiting_queue.begin(), estate->waiting_queue.end(), request);
if (it == estate->waiting_queue.end()) {
TVM_FFI_ICHECK_EQ(rstate->entries.size(), 1);
estate->postproc_workspace.finished_rsentries.push_back(rstate->entries[0]);
}
}
}
ProcessFinishedRequestStateEntries(estate->postproc_workspace.finished_rsentries, estate, models,
max_single_sequence_length, draft_token_workspace_manager,
&estate->postproc_workspace.callback_delta_outputs);
if (!estate->postproc_workspace.callback_delta_outputs.empty()) {
NVTXScopedRange nvtx_scope("Call request stream callback");
// - Invoke the stream callback function once for all collected requests.
request_stream_callback(estate->postproc_workspace.callback_delta_outputs);
}
} // namespace serve
RequestStateEntry PreemptLastRunningRequestStateEntry(
EngineState estate, const Array<Model>& models,
Optional<DraftTokenWorkspaceManager> draft_token_workspace_manager,
Optional<EventTraceRecorder> trace_recorder) {
TVM_FFI_ICHECK(!estate->running_queue.empty());
Request request = estate->running_queue.back();
// Find the last alive request state entry, which is what we want to preempt.
RequestState rstate = estate->GetRequestState(request);
int preempt_rstate_idx = -1;
for (int i = static_cast<int>(rstate->entries.size()) - 1; i >= 0; --i) {
if (rstate->entries[i]->status == RequestStateStatus::kAlive) {
preempt_rstate_idx = i;
break;
}
}
TVM_FFI_ICHECK_NE(preempt_rstate_idx, -1);
RequestStateEntry rsentry = rstate->entries[preempt_rstate_idx];
if (estate->disaggregation) {
AbortRequestImpl(estate, models, request->id, "preempt");
return rsentry;
}
// When the request state entry still has pending inputs,
// it means the request is still in the waiting queue.
bool partially_alive = !rsentry->mstates[0]->inputs.empty();
// Remove from models.
// - Clear model speculation draft.
// - Update `inputs` for future prefill.
RECORD_EVENT(trace_recorder, rsentry->request->id, "preempt");
rsentry->status = RequestStateStatus::kPending;
std::vector<int> draft_token_slots;
for (RequestModelState mstate : rsentry->mstates) {
if (draft_token_workspace_manager.has_value()) {
mstate->RemoveAllDraftTokens(&draft_token_slots);
draft_token_workspace_manager.value()->FreeSlots(draft_token_slots);
}
// If the commited tokens of the current model lags behind the
// committed tokens of the main model (models[0]), we commit those
// new tokens to this model.
for (size_t i = mstate->committed_tokens.size();
i < rsentry->mstates[0]->committed_tokens.size(); ++i) {
mstate->CommitToken(rsentry->mstates[0]->committed_tokens[i]);
}
std::vector<int32_t> committed_token_ids;
committed_token_ids.reserve(mstate->committed_tokens.size());
for (const SampleResult& committed_token : mstate->committed_tokens) {
committed_token_ids.push_back(committed_token.GetTokenId());
}
mstate->num_prefilled_tokens = 0;
Array<Data> inputs;
if (rsentry->parent_idx == -1) {
inputs = request->inputs;
if (const auto* token_input = inputs.back().as<TokenDataNode>()) {
// Merge the TokenData so that a single time TokenEmbed is needed.
std::vector<int> token_ids{token_input->token_ids->data,
token_input->token_ids->data + token_input->token_ids.size()};
token_ids.insert(token_ids.end(), committed_token_ids.begin(), committed_token_ids.end());
inputs.Set(static_cast<int64_t>(inputs.size()) - 1, TokenData(token_ids));
} else if (!committed_token_ids.empty()) {
inputs.push_back(TokenData(committed_token_ids));
}
} else if (!committed_token_ids.empty()) {
inputs.push_back(TokenData(committed_token_ids));
}
mstate->inputs = std::move(inputs);
mstate->prefilled_inputs.clear();
mstate->cached_committed_tokens = 0;
mstate->num_tokens_for_next_decode = 0;
}
if (estate->prefix_cache->HasSequence(rsentry->mstates[0]->internal_id)) {
estate->prefix_cache->RecycleSequence(rsentry->mstates[0]->internal_id, /*lazy=*/false);
} else {
RemoveRequestFromModel(estate, rsentry->mstates[0]->internal_id, models);
}
// Since the sequence has been removed from model, assign a new sequence ID.
int64_t new_seq_id = estate->id_manager.GetNewId();
for (RequestModelState mstate : rsentry->mstates) {
mstate->internal_id = new_seq_id;
}
if (preempt_rstate_idx == 0) {
// Remove from running queue.
estate->running_queue.erase(estate->running_queue.end() - 1);
}
if (!partially_alive && preempt_rstate_idx == static_cast<int>(rstate->entries.size()) - 1) {
// Add to the front of waiting queue.
estate->waiting_queue.insert(estate->waiting_queue.begin(), request);
}
estate->running_rsentries_changed = true;
return rsentry;
}
std::pair<Tensor, std::vector<SampleResult>> ApplyLogitProcessorAndSample(
const LogitProcessor& logit_processor, const Sampler& sampler, const Tensor& logits,
const Array<GenerationConfig>& generation_cfg, const Array<String>& request_ids,
const Array<RequestModelState>& mstates, const std::vector<RandomGenerator*>& rngs,
const std::vector<int>& sample_indices, const Array<GenerationConfig>& child_generation_cfg,
const Array<String>& child_request_ids, const std::vector<int>& child_sample_indices) {
// - Update logits.
logit_processor->InplaceUpdateLogits(logits, generation_cfg, mstates, request_ids);
// - Compute probability distributions.
Tensor probs_on_device =
logit_processor->ComputeProbsFromLogits(logits, generation_cfg, request_ids);
// - Sample tokens.
Tensor renormalized_probs = sampler->BatchRenormalizeProbsByTopP(probs_on_device, sample_indices,
request_ids, generation_cfg);
std::vector<SampleResult> sample_results = sampler->BatchSampleTokensWithProbAfterTopP(
renormalized_probs, child_sample_indices, child_request_ids, child_generation_cfg, rngs);
return {std::move(probs_on_device), std::move(sample_results)};
}
} // namespace serve
} // namespace llm
} // namespace mlc
+119
View File
@@ -0,0 +1,119 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/action_commons.h
* \brief Common functions that may be used in multiple EngineActions.
*/
#ifndef MLC_LLM_SERVE_ENGINE_ACTIONS_ACTION_COMMONS_H_
#define MLC_LLM_SERVE_ENGINE_ACTIONS_ACTION_COMMONS_H_
#include <tvm/ffi/container/array.h>
#include "../../tokenizers/tokenizers.h"
#include "../draft_token_workspace_manager.h"
#include "../engine.h"
#include "../engine_state.h"
#include "../event_trace_recorder.h"
#include "../model.h"
#include "action.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
using tvm::ffi::Object;
/*! \brief Create the engine actions based on engine config. */
Array<EngineAction> CreateEngineActions(Array<Model> models, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
std::vector<ModelWorkspace> model_workspaces,
LogitProcessor logit_processor, Sampler sampler,
DraftTokenWorkspaceManager draft_token_workspace_manager,
Tokenizer tokenizer,
Optional<EventTraceRecorder> trace_recorder,
FRequestStreamCallback request_stream_callback,
Device device);
/*!
* \brief Remove the given request from models.
* \param estate The engine state to update after removal.
* \param req_internal_id The internal id of the request to remove.
* \param models The models to remove the given request from.
*/
void RemoveRequestFromModel(EngineState estate, int64_t req_internal_id,
const Array<Model>& models);
/*!
* \brief The request post-processing after an engine action step.
* It includes
* - invoke the request function callback to return new generated tokens,
* - update the engine state for finished requests.
* \note This function may remove requests from the `running_queue`.
* \param requests The requests to process.
* \param estate The engine state.
* \param models The models to remove the finished from.
* \param tokenizer The tokenizer for logprob process.
* \param request_stream_callback The request stream callback function.
* \param max_single_sequence_length The max single sequence length to help decide
* \param draft_token_workspace_manager The draft token workspace manager.
* \param trace_recorder The event trace recorder for requests.
* if a request is finished.
*/
void ActionStepPostProcess(Array<Request> requests, EngineState estate, const Array<Model>& models,
const Tokenizer& tokenizer,
FRequestStreamCallback request_stream_callback,
int64_t max_single_sequence_length,
Optional<DraftTokenWorkspaceManager> draft_token_workspace_manager,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Preempt the last running request state entry from `running_queue`.
* If all entries of the selected request have been preempted,
* remove it from running request.
* If it is not in the waiting request queue, add it to the waiting queue.
* \param estate The engine state to update due to preemption.
* \param models The models to remove preempted requests from.
* \param draft_token_workspace_manager The draft token workspace manager for requests. Must be
* provided if speculative decoding is enabled.
* \param trace_recorder The event trace recorder for requests.
* \return The preempted request state.
*/
RequestStateEntry PreemptLastRunningRequestStateEntry(
EngineState estate, const Array<Model>& models,
Optional<DraftTokenWorkspaceManager> draft_token_workspace_manager,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Apply the logit processor to the logits and sample one token for each request.
*
* Both the parent request configurations and the child request configurations need to be provided.
* The parent request configurations are used to process the logits, normalize the probabilities.
* The child request configurations are used to sample the tokens.
*
* When the request doesn't have children, the parent and child configurations are the same.
*
* \param logit_processor The logit processor to apply.
* \param sampler The sampler to sample tokens.
* \param logits The logits to process.
* \param generation_cfg The generation configurations of the requests.
* \param request_ids The request ids.
* \param mstates The model states of the requests.
* \param rngs The random generators of the requests.
* \param sample_indices The indices of the requests to sample.
* \param child_generation_cfg The generation configurations of the child requests.
* \param child_request_ids The request ids of the child requests.
* \param child_sample_indices The indices of the child requests to sample.
* \return The processed logits and the sampled results.
*/
std::pair<Tensor, std::vector<SampleResult>> ApplyLogitProcessorAndSample(
const LogitProcessor& logit_processor, const Sampler& sampler, const Tensor& logits,
const Array<GenerationConfig>& generation_cfg, const Array<String>& request_ids,
const Array<RequestModelState>& mstates, const std::vector<RandomGenerator*>& rngs,
const std::vector<int>& sample_indices, const Array<GenerationConfig>& child_generation_cfg,
const Array<String>& child_request_ids, const std::vector<int>& child_sample_indices);
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_ENGINE_ACTIONS_ACTION_COMMONS_H_
@@ -0,0 +1,88 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/auto_spec_decode.cc
*/
#include <numeric>
#include "../config.h"
#include "action.h"
namespace mlc {
namespace llm {
namespace serve {
/*!
* \brief The action that first makes a decision on whether to run speculative
* decoding or normal mode batch decode, and then runs the selected actions.
*/
class AutoSpecDecodeActionObj : public EngineActionObj {
public:
explicit AutoSpecDecodeActionObj(Array<EngineAction> spec_decode_actions,
Array<EngineAction> batch_decode_actions,
EngineConfig engine_config)
: spec_decode_actions_(std::move(spec_decode_actions)),
batch_decode_actions_(std::move(batch_decode_actions)),
engine_config_(std::move(engine_config)) {}
Array<Request> Step(EngineState estate) final {
int num_running_rsentries = estate->GetRunningRequestStateEntries().size();
if (num_running_rsentries == 0) {
return {};
}
// Calculate the draft length to use for the next round decode.
estate->spec_draft_length = CalculateDraftLength(estate, num_running_rsentries);
TVM_FFI_ICHECK_GE(estate->spec_draft_length, 0);
Array<Request> processed_requests;
// Use speculative decoding when the computed draft length is positive.
// Otherwise use normal mode batch decode.
Array<EngineAction> actions =
estate->spec_draft_length > 0 ? spec_decode_actions_ : batch_decode_actions_;
for (EngineAction action : actions) {
processed_requests = action->Step(estate);
}
// Reset the draft length.
estate->spec_draft_length = 0;
return processed_requests;
}
private:
int CalculateDraftLength(EngineState estate, int num_running_rsentries) {
// Right now we use the fixed table to select the draft length (only based on
// the batch size). We will follow up to adopt powerful draft length selection.
int draft_length = 0;
if (num_running_rsentries < 10) {
draft_length = 4;
} else if (num_running_rsentries < 20) {
draft_length = 3;
} else if (num_running_rsentries < 30) {
draft_length = 2;
} else {
draft_length = 0;
}
int effective_batch_size = num_running_rsentries * (draft_length + 1);
return effective_batch_size > engine_config_->max_num_sequence ? 0 : draft_length;
}
/*! \brief The speculative decode actions. */
Array<EngineAction> spec_decode_actions_;
/*! \brief The normal mode decode actions. */
Array<EngineAction> batch_decode_actions_;
/*! \brief The engine config. */
EngineConfig engine_config_;
};
EngineAction EngineAction::AutoSpecDecode(std::vector<EngineAction> spec_decode_actions_,
std::vector<EngineAction> batch_decode_actions_,
EngineConfig engine_config) {
return EngineAction(tvm::ffi::make_object<AutoSpecDecodeActionObj>(
Array<EngineAction>(spec_decode_actions_), Array<EngineAction>(batch_decode_actions_),
std::move(engine_config)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
+329
View File
@@ -0,0 +1,329 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/batch_decode.cc
*/
#include <tvm/support/cuda/nvtx.h>
#include <numeric>
#include "../../support/random.h"
#include "../config.h"
#include "../model.h"
#include "../sampler/sampler.h"
#include "action.h"
#include "action_commons.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
/*!
* \brief The action that runs one-step decode for requests in the
* `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
* \note The BatchDecode action **does not** take effect for speculative
* decoding scenarios where there are multiple models. For speculative
* decoding in the future, we will use other specific actions.
*/
class BatchDecodeActionObj : public EngineActionObj {
public:
explicit BatchDecodeActionObj(Array<Model> models, Tokenizer tokenizer,
LogitProcessor logit_processor, Sampler sampler,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder)
: models_(std::move(models)),
tokenizer_(std::move(tokenizer)),
logit_processor_(std::move(logit_processor)),
sampler_(std::move(sampler)),
engine_config_(std::move(engine_config)),
trace_recorder_(std::move(trace_recorder)) {}
Array<Request> Step(EngineState estate) final {
// - Do not run decode when there is no running request.
if (estate->running_queue.empty()) {
return {};
}
// Preempt request state entries when decode cannot apply.
std::vector<RequestStateEntry> running_rsentries;
{
NVTXScopedRange nvtx_scope("BatchDecode getting requests");
running_rsentries = estate->GetRunningRequestStateEntries();
while (!CanDecode(running_rsentries.size())) {
if (estate->prefix_cache->TryFreeMemory()) continue;
RequestStateEntry preempted =
PreemptLastRunningRequestStateEntry(estate, models_, std::nullopt, trace_recorder_);
if (preempted.same_as(running_rsentries.back())) {
running_rsentries.pop_back();
}
}
while (running_rsentries.size() >
std::min(static_cast<int64_t>(engine_config_->max_num_sequence),
engine_config_->prefill_chunk_size)) {
running_rsentries.pop_back();
}
}
auto tstart = std::chrono::high_resolution_clock::now();
// NOTE: Right now we only support decode all the running request states at a time.
int num_rsentries = running_rsentries.size();
TVM_FFI_ICHECK_GT(num_rsentries, 0)
<< "There should be at least one request state entry that can run decode. "
"Possible failure reason: none of the prefill phase of the running requests is finished";
TVM_FFI_ICHECK_LE(num_rsentries, engine_config_->max_num_sequence)
<< "The number of running requests exceeds the max number of sequence in EngineConfig. "
"Possible failure reason: the prefill action allows new sequence in regardless of the "
"max num sequence.";
// Collect
// - the last committed token,
// - the request id,
// - the generation config,
// - the random number generator,
// of each request state entry.
std::vector<int> input_tokens;
std::vector<int> lengths;
Array<String> request_ids;
std::vector<int64_t> request_internal_ids;
Array<RequestModelState> mstates;
Array<GenerationConfig> generation_cfg;
std::vector<RandomGenerator*> rngs;
input_tokens.reserve(num_rsentries);
request_ids.reserve(num_rsentries);
request_internal_ids.reserve(num_rsentries);
mstates.reserve(num_rsentries);
generation_cfg.reserve(num_rsentries);
rngs.reserve(num_rsentries);
{
NVTXScopedRange nvtx_scope("BatchDecode setting batch info");
for (const RequestStateEntry& rsentry : running_rsentries) {
auto mstate = rsentry->mstates[0];
TVM_FFI_ICHECK(mstate->num_tokens_for_next_decode > 0 &&
mstate->num_tokens_for_next_decode <=
static_cast<int>(mstate->committed_tokens.size()));
for (auto begin = mstate->committed_tokens.end() - mstate->num_tokens_for_next_decode;
begin != mstate->committed_tokens.end(); ++begin) {
input_tokens.push_back(begin->GetTokenId());
}
lengths.push_back(mstate->num_tokens_for_next_decode);
mstate->num_tokens_for_next_decode = 0;
request_ids.push_back(rsentry->request->id);
request_internal_ids.push_back(mstate->internal_id);
mstates.push_back(mstate);
generation_cfg.push_back(rsentry->request->generation_cfg);
rngs.push_back(&rsentry->rng);
}
}
// - Compute embeddings.
RECORD_EVENT(trace_recorder_, request_ids, "start embedding");
ObjectRef embeddings =
models_[0]->TokenEmbed({Shape(input_tokens.begin(), input_tokens.end())});
RECORD_EVENT(trace_recorder_, request_ids, "finish embedding");
// - Invoke model decode.
// If every request only requires to process one token, batch decode kernel is called.
// Otherwise, batch prefill kernel is called.
bool is_every_request_single_token =
std::all_of(lengths.begin(), lengths.end(), [](int len) { return len == 1; });
RECORD_EVENT(trace_recorder_, request_ids, "start decode");
Tensor logits;
if (is_every_request_single_token) {
logits = models_[0]->BatchDecode(embeddings, request_internal_ids);
TVM_FFI_ICHECK_EQ(logits->ndim, 3);
TVM_FFI_ICHECK_EQ(logits->shape[0], num_rsentries);
TVM_FFI_ICHECK_EQ(logits->shape[1], 1);
} else {
logits = models_[0]->BatchPrefill(embeddings, request_internal_ids, lengths);
TVM_FFI_ICHECK_EQ(logits->ndim, 3);
TVM_FFI_ICHECK_EQ(logits->shape[0], 1);
TVM_FFI_ICHECK_EQ(logits->shape[1], num_rsentries);
}
RECORD_EVENT(trace_recorder_, request_ids, "finish decode");
// - Update logits.
logits = logits.CreateView({num_rsentries, logits->shape[2]}, logits->dtype);
logit_processor_->InplaceUpdateLogits(logits, generation_cfg, mstates, request_ids);
// - Compute probability distributions.
Tensor probs_on_device =
logit_processor_->ComputeProbsFromLogits(logits, generation_cfg, request_ids);
// - Commit the prefix cache changes from previous round of action.
// Note: we commit prefix cache changes here to overlap this commit with the GPU execution.
estate->prefix_cache->CommitSequenceExtention();
// - Sample tokens.
// Fill range [0, num_rsentries) into `sample_indices`.
std::vector<int> sample_indices(num_rsentries);
std::iota(sample_indices.begin(), sample_indices.end(), 0);
Tensor renormalized_probs = sampler_->BatchRenormalizeProbsByTopP(
probs_on_device, sample_indices, request_ids, generation_cfg);
std::vector<SampleResult> sample_results = sampler_->BatchSampleTokensWithProbAfterTopP(
renormalized_probs, sample_indices, request_ids, generation_cfg, rngs);
TVM_FFI_ICHECK_EQ(sample_results.size(), num_rsentries);
// - Update the committed tokens of states.
for (int i = 0; i < num_rsentries; ++i) {
auto mstate = mstates[i];
if (!mstate->require_retokenization_in_next_decode) {
mstates[i]->CommitToken(sample_results[i]);
// live update the output metrics
running_rsentries[i]->rstate->metrics.completion_tokens += 1;
} else {
// Retokenize and commit tokens.
CommitTokenMayRetokenize(running_rsentries[i], mstate, sample_results[i]);
mstate->require_retokenization_in_next_decode = false;
}
running_rsentries[i]->rstate->metrics.decode_tokens += lengths[i];
}
double elapsed_time;
{
NVTXScopedRange nvtx_scope("BatchDecode get time");
auto tend = std::chrono::high_resolution_clock::now();
elapsed_time = static_cast<double>((tend - tstart).count()) / 1e9;
}
estate->metrics.engine_decode_time_sum += elapsed_time;
estate->metrics.UpdateDecodeTimeByBatchSize(num_rsentries, elapsed_time);
return estate->running_queue;
}
private:
/*! \brief Check if the input request state entries can be decoded under conditions. */
bool CanDecode(int num_rsentries) {
int num_available_pages = models_[0]->GetNumAvailablePages();
return num_rsentries <= num_available_pages;
}
/*!
* \brief Retokenize the past tokens with a new token.
* \param mstate The model state.
* \param token_id The new token id.
* \param max_rollback_tokens The maximum number of tokens to rollback.
* \return The number of tokens to rollback and the new tokens.
*/
std::pair<int, std::vector<int32_t>> RetokenizeWithNewToken(RequestModelState mstate,
int32_t token_id,
int max_rollback_tokens) {
// Step 1. Get past tokens
// past_tokens = mstate[-max_rollback_tokens:]
// past_string = detokenize(past_tokens)
const auto& token_table = tokenizer_->PostProcessedTokenTable();
std::vector<int32_t> past_tokens;
std::string past_string;
auto past_begin_it = mstate->committed_tokens.size() >= max_rollback_tokens
? mstate->committed_tokens.end() - max_rollback_tokens
: mstate->committed_tokens.begin();
for (auto it = past_begin_it; it != mstate->committed_tokens.end(); ++it) {
past_tokens.push_back(it->GetTokenId());
past_string += token_table[it->GetTokenId()];
}
// Step 2. Retokenize
// Compare tokenize(past_string + new_string) and past_tokens
auto new_tokens = tokenizer_->EncodeNoPrependSpace(past_string + token_table[token_id]);
int first_differ_idx = past_tokens.size();
for (int i = 0; i < static_cast<int>(past_tokens.size()); ++i) {
if (i == static_cast<int>(new_tokens.size()) || past_tokens[i] != new_tokens[i]) {
first_differ_idx = i;
break;
}
}
return {past_tokens.size() - first_differ_idx,
std::vector<int32_t>(new_tokens.begin() + first_differ_idx, new_tokens.end())};
}
/*!
* \brief Commit the token and may retokenize the past tokens.
* \param rsentry The request state entry.
* \param mstate The model state.
* \param sample_result The sampled token.
*/
void CommitTokenMayRetokenize(RequestStateEntry rsentry, RequestModelState mstate,
const SampleResult& sample_result) {
auto generation_cfg = rsentry->request->generation_cfg;
// 1. If EOS token is generated, jump commit it
if (!generation_cfg->debug_config.ignore_eos &&
std::any_of(generation_cfg->stop_token_ids.begin(), generation_cfg->stop_token_ids.end(),
[&](int32_t token) { return token == sample_result.GetTokenId(); })) {
mstate->CommitToken(sample_result);
rsentry->rstate->metrics.completion_tokens += 1;
return;
}
// 2. Check retokenization
const auto& committed_tokens = mstate->committed_tokens;
auto [rollback_cnt, new_tokens] =
RetokenizeWithNewToken(mstate, sample_result.GetTokenId(), MAX_ROLLBACK_TOKENS_);
// 3. Handle output when retokenization happens
if (rollback_cnt >
static_cast<int>(committed_tokens.size()) - rsentry->next_callback_token_pos) {
const auto& token_table = tokenizer_->PostProcessedTokenTable();
for (auto i = rsentry->next_callback_token_pos; i < committed_tokens.size(); ++i) {
auto token_id = committed_tokens[i].GetTokenId();
rsentry->extra_prefix_string += token_table[token_id];
}
rsentry->extra_prefix_string += token_table[sample_result.GetTokenId()];
rsentry->next_callback_token_pos = static_cast<int>(committed_tokens.size()) - rollback_cnt +
static_cast<int>(new_tokens.size());
}
if (rollback_cnt > 0) {
mstate->RollbackTokens(rollback_cnt);
models_[0]->PopNFromKVCache(mstate->internal_id, rollback_cnt);
}
for (auto token_id : new_tokens) {
mstate->CommitToken({{token_id, 1.0}, {}});
}
rsentry->rstate->metrics.completion_tokens +=
static_cast<int>(new_tokens.size()) - rollback_cnt;
}
/*!
* \brief The model to run decode in. When there are multiple
* models, the `Step` function of the created action will not take effect.
*/
Array<Model> models_;
/*! \brief The tokenizer of the engine. */
Tokenizer tokenizer_;
/*! \brief The logit processor. */
LogitProcessor logit_processor_;
/*! \brief The sampler to sample new tokens. */
Sampler sampler_;
/*! \brief The engine config. */
EngineConfig engine_config_;
/*! \brief Event trace recorder. */
Optional<EventTraceRecorder> trace_recorder_;
/*! \brief The maximum number of tokens to retokenize and may be rolled back. */
const int MAX_ROLLBACK_TOKENS_ = 10;
};
EngineAction EngineAction::BatchDecode(Array<Model> models, Tokenizer tokenizer,
LogitProcessor logit_processor, Sampler sampler,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder) {
return EngineAction(tvm::ffi::make_object<BatchDecodeActionObj>(
std::move(models), std::move(tokenizer), std::move(logit_processor), std::move(sampler),
std::move(engine_config), std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
+407
View File
@@ -0,0 +1,407 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/batch_draft.cc
*/
#include <numeric>
#include "../config.h"
#include "../model.h"
#include "../sampler/sampler.h"
#include "action.h"
#include "action_commons.h"
namespace mlc {
namespace llm {
namespace serve {
/*!
* \brief The action that runs draft proposal for requests in the
* `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
*/
class BatchDraftActionObj : public EngineActionObj {
public:
explicit BatchDraftActionObj(Array<Model> models, LogitProcessor logit_processor, Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder)
: models_(std::move(models)),
logit_processor_(std::move(logit_processor)),
sampler_(std::move(sampler)),
model_workspaces_(std::move(model_workspaces)),
draft_token_workspace_manager_(std::move(draft_token_workspace_manager)),
engine_config_(std::move(engine_config)),
trace_recorder_(std::move(trace_recorder)) {}
Array<Request> Step(EngineState estate) final {
// - Only run spec decode when there are two models (llm+ssm) and >=1 running requests.
if (models_.size() != 2 || estate->running_queue.empty()) {
return {};
}
// Preempt request state entries when decode cannot apply.
std::vector<RequestStateEntry> running_rsentries = estate->GetRunningRequestStateEntries();
while (!CanDecode(running_rsentries.size())) {
if (estate->prefix_cache->TryFreeMemory()) continue;
RequestStateEntry preempted = PreemptLastRunningRequestStateEntry(
estate, models_, draft_token_workspace_manager_, trace_recorder_);
if (preempted.same_as(running_rsentries.back())) {
running_rsentries.pop_back();
}
}
while (running_rsentries.size() * (engine_config_->spec_draft_length + 1) >
std::min(static_cast<int64_t>(engine_config_->max_num_sequence),
engine_config_->prefill_chunk_size)) {
running_rsentries.pop_back();
}
auto tstart = std::chrono::high_resolution_clock::now();
int num_rsentries = running_rsentries.size();
TVM_FFI_ICHECK_GT(num_rsentries, 0)
<< "There should be at least one request state entry that can run decode. "
"Possible failure reason: none of the prefill phase of the running requests is finished";
TVM_FFI_ICHECK_LE(num_rsentries, engine_config_->max_num_sequence)
<< "The number of running requests exceeds the max number of sequence in EngineConfig. "
"Possible failure reason: the prefill action allows new sequence in regardless of the "
"max num sequence.";
Array<String> request_ids;
std::vector<int64_t> request_internal_ids;
Array<String> request_ids_per_leaf_node;
Array<GenerationConfig> generation_cfg;
Array<GenerationConfig> generation_cfg_for_logitproc;
std::vector<RandomGenerator*> rngs;
std::vector<std::vector<int>> draft_token_indices;
// Number of input tokens for each request. Each request can have multiple leaf tokens for the
// next forward when multiple tokens are drafted.
std::vector<int> cum_num_tokens;
std::vector<int64_t> token_tree_parent_ptr;
request_ids.reserve(num_rsentries);
request_internal_ids.reserve(num_rsentries);
generation_cfg.reserve(num_rsentries);
generation_cfg_for_logitproc.reserve(num_rsentries);
draft_token_indices.reserve(num_rsentries);
cum_num_tokens.reserve(num_rsentries + 1);
for (const RequestStateEntry& rsentry : running_rsentries) {
request_ids.push_back(rsentry->request->id);
request_internal_ids.push_back(rsentry->mstates[0]->internal_id);
}
TVM_FFI_ICHECK_GT(estate->spec_draft_length, 0)
<< "The speculative decoding draft length must be positive.";
// The first model doesn't get involved in draft proposal.
for (int model_id = 1; model_id < static_cast<int>(models_.size()); ++model_id) {
// Collect
// - the last committed token,
// - the request model state of each request,
// - the number of tokens for each request to send into the model (it may
// be more than one if the draft model is lagging behind the main model, when
// the engine switches from normal batch decode mode to speculative decoding mode).
std::vector<int> input_tokens;
Array<RequestModelState> mstates;
std::vector<int> input_lengths;
input_tokens.reserve(num_rsentries);
mstates.reserve(num_rsentries);
input_lengths.reserve(num_rsentries);
for (const RequestStateEntry& rsentry : running_rsentries) {
mstates.push_back(rsentry->mstates[model_id]);
}
// "Draft length" rounds of draft proposal.
for (int draft_id = 0; draft_id < estate->spec_draft_length; ++draft_id) {
auto tdraft_start = std::chrono::high_resolution_clock::now();
// prepare new input tokens
input_tokens.clear();
input_lengths.clear();
token_tree_parent_ptr.clear();
generation_cfg.clear();
generation_cfg_for_logitproc.clear();
rngs.clear();
cum_num_tokens.clear();
cum_num_tokens.push_back(0);
request_ids_per_leaf_node.clear();
std::vector<int> draft_token_parent_idx;
draft_token_indices.clear();
if (draft_id == 0) {
// Compute the total length that needs to be processed by the draft model,
// including the lagging-behind part of hte draft model.
// When the total length to be processed is larger than the prefill chunk
// size, we must do the prefill with multiple rounds by chunk.
int total_length = 0;
for (int i = 0; i < num_rsentries; ++i) {
TVM_FFI_ICHECK_LE(mstates[i]->committed_tokens.size(),
running_rsentries[i]->mstates[0]->committed_tokens.size());
total_length += running_rsentries[i]->mstates[0]->committed_tokens.size() -
mstates[i]->committed_tokens.size() + 1;
}
if (total_length > engine_config_->prefill_chunk_size) {
PrefillLaggedTokensByChunk(mstates, running_rsentries, models_[model_id],
total_length - engine_config_->prefill_chunk_size);
}
}
for (int i = 0; i < num_rsentries; ++i) {
int num_leaf_nodes = 0;
// Starting from last committed tokens
if (draft_id == 0) {
TVM_FFI_ICHECK_LE(mstates[i]->committed_tokens.size(),
running_rsentries[i]->mstates[0]->committed_tokens.size());
TVM_FFI_ICHECK_EQ(mstates[i]->num_tokens_for_next_decode, 1);
input_tokens.push_back(mstates[i]->committed_tokens.back().GetTokenId());
input_lengths.push_back(running_rsentries[i]->mstates[0]->committed_tokens.size() -
mstates[i]->committed_tokens.size() + 1);
for (size_t j = mstates[i]->committed_tokens.size();
j < running_rsentries[i]->mstates[0]->committed_tokens.size(); ++j) {
// This draft model is lagging behind the main model.
// It may happen when the engine just switches from the normal batch decode
// mode to the speculative decoding mode.
// In this case, we need to prefill the misaligned tokens into the draft model.
mstates[i]->CommitToken(running_rsentries[i]->mstates[0]->committed_tokens[j]);
input_tokens.push_back(
running_rsentries[i]->mstates[0]->committed_tokens[j].GetTokenId());
}
mstates[i]->num_tokens_for_next_decode = 0;
draft_token_indices.emplace_back(std::vector<int>{-1});
rngs.push_back(&running_rsentries[i]->rng);
draft_token_parent_idx.push_back(-1);
request_ids_per_leaf_node.push_back(request_ids[i]);
num_leaf_nodes = 1;
cum_num_tokens.push_back(cum_num_tokens.back() + 1);
} else {
TVM_FFI_ICHECK_EQ(mstates[i]->committed_tokens.size(),
running_rsentries[i]->mstates[0]->committed_tokens.size());
TVM_FFI_ICHECK(!mstates[i]->draft_output_tokens.empty());
draft_token_indices.emplace_back(std::vector<int>{});
// Get all leaf nodes
for (int j = 0; j < static_cast<int>(mstates[i]->draft_output_tokens.size()); ++j) {
if (mstates[i]->draft_token_first_child_idx[j] == -1) {
int64_t parent_idx = mstates[i]->draft_token_parent_idx[j];
token_tree_parent_ptr.push_back(parent_idx);
input_tokens.push_back(mstates[i]->draft_output_tokens[j].GetTokenId());
draft_token_indices.back().push_back(j);
rngs.push_back(&running_rsentries[i]->rng);
num_leaf_nodes++;
request_ids_per_leaf_node.push_back(request_ids[i]);
draft_token_parent_idx.push_back(j);
}
}
input_lengths.push_back(num_leaf_nodes);
cum_num_tokens.push_back(cum_num_tokens.back() + input_lengths.back());
}
GenerationConfig generation_cfg_for_draft = [&]() {
if (engine_config_->spec_tree_width == 1) {
return mstates[i]->request->generation_cfg;
}
auto spec_generation_cfg = tvm::ffi::make_object<GenerationConfigNode>(
*(mstates[i]->request->generation_cfg.get()));
spec_generation_cfg->top_logprobs = engine_config_->spec_tree_width;
spec_generation_cfg->logprobs = true;
spec_generation_cfg->temperature = 1.0;
return GenerationConfig(spec_generation_cfg);
}();
for (int j = 0; j < num_leaf_nodes; ++j) {
generation_cfg.push_back(generation_cfg_for_draft);
}
generation_cfg_for_logitproc.push_back(generation_cfg_for_draft);
}
// - Compute embeddings.
RECORD_EVENT(trace_recorder_, request_ids, "start proposal embedding");
TVM_FFI_ICHECK_LE(input_tokens.size(), engine_config_->prefill_chunk_size);
ObjectRef embeddings =
models_[model_id]->TokenEmbed({Shape{input_tokens.begin(), input_tokens.end()}});
RECORD_EVENT(trace_recorder_, request_ids, "finish proposal embedding");
// - Invoke model decode.
RECORD_EVENT(trace_recorder_, request_ids, "start proposal decode");
Tensor logits{nullptr};
if (input_tokens.size() == num_rsentries) {
// Each request entry only has one token to feed into the draft model.
logits = models_[model_id]->BatchDecode(embeddings, request_internal_ids);
TVM_FFI_ICHECK_EQ(logits->ndim, 3);
TVM_FFI_ICHECK_EQ(logits->shape[0], num_rsentries);
TVM_FFI_ICHECK_EQ(logits->shape[1], 1);
} else if (draft_id == 0) {
// There exists some request entry which has more than one token to feed.
// It may happen when the engine just switches from the normal batch decode
// mode to the speculative decoding mode.
logits = models_[model_id]->BatchPrefill(embeddings, request_internal_ids, input_lengths);
TVM_FFI_ICHECK_EQ(logits->ndim, 3);
TVM_FFI_ICHECK_EQ(logits->shape[0], 1);
TVM_FFI_ICHECK_EQ(logits->shape[1], num_rsentries);
} else {
TVM_FFI_ICHECK_GT(engine_config_->spec_tree_width, 1);
logits = models_[model_id]->BatchTreeDecode(embeddings, request_internal_ids,
input_lengths, token_tree_parent_ptr);
TVM_FFI_ICHECK_EQ(logits->ndim, 3);
TVM_FFI_ICHECK_EQ(logits->shape[0], cum_num_tokens.back());
TVM_FFI_ICHECK_EQ(logits->shape[1], 1);
}
TVM_FFI_ICHECK_EQ(input_lengths.size(), num_rsentries);
RECORD_EVENT(trace_recorder_, request_ids, "finish proposal decode");
// - Update logits.
logits = logits.CreateView({cum_num_tokens.back(), logits->shape[2]}, logits->dtype);
logit_processor_->InplaceUpdateLogits(logits, generation_cfg_for_logitproc, mstates,
request_ids, &cum_num_tokens, &mstates,
&draft_token_indices);
// - Compute probability distributions.
Tensor probs_on_device = logit_processor_->ComputeProbsFromLogits(
logits, generation_cfg_for_logitproc, request_ids, &cum_num_tokens);
// - Commit the prefix cache changes from previous round of action.
// Note: we commit prefix cache changes here to overlap this commit with the GPU execution.
estate->prefix_cache->CommitSequenceExtention();
// - Sample tokens.
// Fill range [0, num_rsentries) into `sample_indices`.
std::vector<int> sample_indices(cum_num_tokens.back());
std::iota(sample_indices.begin(), sample_indices.end(), 0);
std::vector<Tensor> prob_dist;
Tensor renormalized_probs = sampler_->BatchRenormalizeProbsByTopP(
probs_on_device, sample_indices, request_ids_per_leaf_node, generation_cfg);
std::vector<SampleResult> sample_results = sampler_->BatchSampleTokensWithProbAfterTopP(
renormalized_probs, sample_indices, request_ids_per_leaf_node, generation_cfg, rngs);
TVM_FFI_ICHECK_EQ(sample_results.size(), cum_num_tokens.back());
// - Add draft token to the state.
draft_token_workspace_manager_->AllocSlots(cum_num_tokens.back(), &draft_token_slots_);
models_[model_id]->ScatterDraftProbs(probs_on_device, draft_token_slots_,
&model_workspaces_[0].draft_probs_storage);
for (int i = 0; i < num_rsentries; ++i) {
for (int j = cum_num_tokens[i]; j < cum_num_tokens[i + 1]; ++j) {
int parent_idx = draft_token_parent_idx[j];
if (engine_config_->spec_tree_width == 1) {
mstates[i]->AddDraftToken(sample_results[j], draft_token_slots_[j], parent_idx);
continue;
}
for (int k = 0; k < sample_results[j].top_prob_tokens.size(); ++k) {
SampleResult top_k_token{sample_results[j].top_prob_tokens[k]};
mstates[i]->AddDraftToken(top_k_token, draft_token_slots_[j], parent_idx);
}
}
}
auto tdraft_end = std::chrono::high_resolution_clock::now();
estate->metrics.UpdateDraftTimeByBatchSize(
num_rsentries, static_cast<double>((tdraft_end - tdraft_start).count()) / 1e9);
}
}
auto tend = std::chrono::high_resolution_clock::now();
estate->metrics.engine_decode_time_sum += static_cast<double>((tend - tstart).count()) / 1e9;
return {};
}
private:
/*! \brief Check if the input requests can be decoded under conditions. */
bool CanDecode(int num_rsentries) {
// The first model is not involved in draft proposal.
for (int model_id = 1; model_id < static_cast<int>(models_.size()); ++model_id) {
// Check if the model has enough available pages.
int num_available_pages = models_[model_id]->GetNumAvailablePages();
if (num_rsentries > num_available_pages) {
return false;
}
}
return true;
}
void PrefillLaggedTokensByChunk(const Array<RequestModelState>& mstates,
const std::vector<RequestStateEntry>& running_rsentries,
Model model, int remaining_prefill_length) {
int num_rsentries = mstates.size();
std::vector<int> input_tokens;
std::vector<int64_t> request_internal_ids;
std::vector<int> lengths;
input_tokens.reserve(engine_config_->prefill_chunk_size);
request_internal_ids.reserve(num_rsentries);
lengths.reserve(num_rsentries);
auto f_run_prefill = [&model, &input_tokens, &request_internal_ids, &lengths]() {
ObjectRef embeddings = model->TokenEmbed({Shape{input_tokens.begin(), input_tokens.end()}});
model->BatchPrefill(embeddings, request_internal_ids, lengths);
};
for (int i = 0; i < num_rsentries; ++i) {
int prefill_length =
std::min({static_cast<int>(running_rsentries[i]->mstates[0]->committed_tokens.size() -
mstates[i]->committed_tokens.size()),
static_cast<int>(engine_config_->prefill_chunk_size - input_tokens.size()),
remaining_prefill_length});
if (prefill_length == 0) {
// This rsentry is done.
continue;
}
TVM_FFI_ICHECK(!mstates[i]->committed_tokens.empty());
for (size_t j = mstates[i]->committed_tokens.size();
j < running_rsentries[i]->mstates[0]->committed_tokens.size(); ++j) {
// Commit the lagging-behind tokens to the draft model.
mstates[i]->CommitToken(running_rsentries[i]->mstates[0]->committed_tokens[j - 1]);
input_tokens.push_back(
running_rsentries[i]->mstates[0]->committed_tokens[j - 1].GetTokenId());
}
lengths.push_back(prefill_length);
request_internal_ids.push_back(running_rsentries[i]->mstates[0]->internal_id);
mstates[i]->num_tokens_for_next_decode = 1;
remaining_prefill_length -= prefill_length;
if (remaining_prefill_length == 0) {
// All rsentries are done.
break;
}
if (input_tokens.size() == engine_config_->prefill_chunk_size) {
// Run prefill if the pending part total length reaches the prefill chunk size.
f_run_prefill();
input_tokens.clear();
request_internal_ids.clear();
lengths.clear();
--i;
continue;
}
}
if (!input_tokens.empty()) {
f_run_prefill();
}
}
/*! \brief The model to run draft generation in speculative decoding. */
Array<Model> models_;
/*! \brief The logit processor. */
LogitProcessor logit_processor_;
/*! \brief The sampler to sample new tokens. */
Sampler sampler_;
/*! \brief The model workspaces. */
std::vector<ModelWorkspace> model_workspaces_;
/*! \brief The draft token workspace manager. */
DraftTokenWorkspaceManager draft_token_workspace_manager_;
/*! \brief The engine config. */
EngineConfig engine_config_;
/*! \brief Event trace recorder. */
Optional<EventTraceRecorder> trace_recorder_;
/*! \brief Temporary buffer to store the slots of the current draft tokens */
std::vector<int> draft_token_slots_;
};
EngineAction EngineAction::BatchDraft(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler, std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder) {
return EngineAction(tvm::ffi::make_object<BatchDraftActionObj>(
std::move(models), std::move(logit_processor), std::move(sampler),
std::move(model_workspaces), std::move(draft_token_workspace_manager),
std::move(engine_config), std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
@@ -0,0 +1,240 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/batch_verify.cc
*/
#include <tvm/support/cuda/nvtx.h>
#include <cmath>
#include <exception>
#include "../config.h"
#include "../model.h"
#include "../sampler/sampler.h"
#include "action.h"
#include "action_commons.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
/*!
* \brief The action that runs verification for requests in the
* `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
*/
class BatchJumpForwardActionObj : public EngineActionObj {
public:
explicit BatchJumpForwardActionObj(Array<Model> models, Tokenizer tokenizer,
Optional<EventTraceRecorder> trace_recorder)
: models_(std::move(models)),
tokenizer_(tokenizer),
trace_recorder_(std::move(trace_recorder)) {}
Array<Request> Step(EngineState estate) final {
// - Do not run decode when there are multiple models or no running requests.
if (models_.size() > 1 || estate->running_queue.empty()) {
return {};
}
// Preempt request state entries when jump-forward decoding cannot apply.
std::vector<RequestStateEntry> running_rsentries;
{
NVTXScopedRange nvtx_scope("BatchJumpForward getting requests");
running_rsentries = estate->GetRunningRequestStateEntries();
while (!CheckMemForJumpForward(running_rsentries.size())) {
if (estate->prefix_cache->TryFreeMemory()) continue;
RequestStateEntry preempted =
PreemptLastRunningRequestStateEntry(estate, models_, std::nullopt, trace_recorder_);
if (preempted.same_as(running_rsentries.back())) {
running_rsentries.pop_back();
}
}
}
if (running_rsentries.empty()) {
return {};
}
auto tstart = std::chrono::high_resolution_clock::now();
for (auto rsentry : running_rsentries) {
if (!CanJumpForward(rsentry)) {
continue;
}
auto mstate = rsentry->mstates[0];
auto jump_forward_str = mstate->grammar_matcher->FindJumpForwardString();
if (jump_forward_str.empty()) {
continue;
}
auto [rollback_cnt, new_tokens, new_string] =
RetokenizeWithNewString(mstate, jump_forward_str, MAX_ROLLBACK_TOKENS_);
HandleRollback(rsentry, mstate, rollback_cnt, new_tokens, new_string);
// Commit new tokens (kv cache is handled in the next decode)
for (auto token_id : new_tokens) {
mstate->CommitToken({{token_id, 1.0}, {}});
}
mstate->require_retokenization_in_next_decode = true;
// Update metrics
rsentry->rstate->metrics.jump_forward_tokens +=
std::max(static_cast<int>(new_tokens.size()) - rollback_cnt, 0);
rsentry->rstate->metrics.completion_tokens +=
static_cast<int>(new_tokens.size()) - rollback_cnt;
}
auto tend = std::chrono::high_resolution_clock::now();
estate->metrics.engine_jump_forward_time_sum +=
static_cast<double>((tend - tstart).count()) / 1e9;
return {};
}
private:
/*! \brief Check if jump-forward decoding can be executed without exceeding the memory limit. */
bool CheckMemForJumpForward(int num_rsentries) {
static constexpr int MAX_AVG_JUMPFORWARD_PAGES_PER_REQUEST = 10;
int num_available_pages = models_[0]->GetNumAvailablePages();
return num_rsentries * MAX_AVG_JUMPFORWARD_PAGES_PER_REQUEST <= num_available_pages;
}
/*! \brief Check if the jump-forward can be executed. When logprobs is requested, or the
* grammar state matcher is not defined, jump-forward is not executed. */
bool CanJumpForward(const RequestStateEntry& rsentry) {
if (rsentry->request->generation_cfg->debug_config.grammar_execution_mode !=
GrammarExecutionMode::kJumpForward) {
return false;
}
if (rsentry->request->generation_cfg->logprobs) {
return false;
}
if (!rsentry->mstates[0]->grammar_matcher) {
return false;
}
return true;
}
/*!
* \brief Retokenize the input string with a new string.
* \param mstate The model state.
* \param new_string The new string to append.
* \param max_rollback_tokens The maximum number of tokens to rollback.
* \return The number of tokens to rollback, the new tokens and a delta string of output (equal to
* new_string if no cutoff happens; shorter than new_string if cutoff happens).
*/
std::tuple<int, std::vector<int32_t>, std::string> RetokenizeWithNewString(
RequestModelState mstate, const std::string& new_string, int max_rollback_tokens) {
// Step 1. Get past tokens
// past_tokens = mstate[-max_rollback_tokens:]
// past_string = detokenize(past_tokens)
const auto& token_table = tokenizer_->PostProcessedTokenTable();
std::vector<int32_t> past_tokens;
std::string past_string;
auto past_begin_it = mstate->committed_tokens.size() >= max_rollback_tokens
? mstate->committed_tokens.end() - max_rollback_tokens
: mstate->committed_tokens.begin();
for (auto it = past_begin_it; it != mstate->committed_tokens.end(); ++it) {
past_tokens.push_back(it->GetTokenId());
past_string += token_table[it->GetTokenId()];
}
// Step 2. Retokenize
// Compare tokenize(past_string + new_string) and past_tokens
auto new_tokens = tokenizer_->EncodeNoPrependSpace(past_string + new_string);
auto delta_string = new_string;
// Pop last token if it is a prefix of another token. That's because such tokens will often
// be rolled back in the next decode, which disturbs the distribution, so we will avoid
// generating them.
if (tokenizer_->GetPrefixTokenMask()[new_tokens.back()]) {
auto last_token = token_table[new_tokens.back()];
if (last_token.length() >= new_string.length()) {
return {0, {}, ""};
}
delta_string = delta_string.substr(0, delta_string.length() - last_token.length());
new_tokens.pop_back();
}
int first_differ_idx = past_tokens.size();
for (int i = 0; i < static_cast<int>(past_tokens.size()); ++i) {
if (i == static_cast<int>(new_tokens.size()) || past_tokens[i] != new_tokens[i]) {
first_differ_idx = i;
break;
}
}
return {past_tokens.size() - first_differ_idx,
std::vector<int32_t>(new_tokens.begin() + first_differ_idx, new_tokens.end()),
delta_string};
}
/*!
* \brief Handle rollback for the stream output, the model state and the kv cache.
* \param rsentry The request state entry.
* \param mstate The model state.
* \param rollback_cnt The number of tokens to rollback.
* \param new_tokens The new tokens. Useful for the stream output.
* \param new_string The delta string of output. Useful for the stream output.
*/
void HandleRollback(const RequestStateEntry& rsentry, RequestModelState mstate, int rollback_cnt,
const std::vector<int32_t>& new_tokens, const std::string& new_string) {
// 1. Handle rollback for the stream output
if (rollback_cnt >
static_cast<int>(mstate->committed_tokens.size()) - rsentry->next_callback_token_pos) {
const auto& token_table = tokenizer_->PostProcessedTokenTable();
for (auto i = rsentry->next_callback_token_pos; i < mstate->committed_tokens.size(); ++i) {
auto token_id = mstate->committed_tokens[i].GetTokenId();
rsentry->extra_prefix_string += token_table[token_id];
}
rsentry->extra_prefix_string += new_string;
rsentry->next_callback_token_pos = static_cast<int>(mstate->committed_tokens.size()) -
rollback_cnt + static_cast<int>(new_tokens.size());
}
// 2. Handle rollback for the model state
if (rollback_cnt > 0) {
mstate->RollbackTokens(rollback_cnt);
}
// 3. Handle rollback for the kv cache
if (rollback_cnt > mstate->num_tokens_for_next_decode) {
models_[0]->PopNFromKVCache(mstate->internal_id,
rollback_cnt - mstate->num_tokens_for_next_decode);
mstate->num_tokens_for_next_decode = 0;
} else {
mstate->num_tokens_for_next_decode -= rollback_cnt;
}
}
/*!
* \brief The model to run jump-forward decoding. When there are multiple
* models, the `Step` function of the created action will not take effect.
*/
Array<Model> models_;
/*! \brief Tokenizer for retokenization. */
Tokenizer tokenizer_;
/*! \brief Event trace recorder. */
Optional<EventTraceRecorder> trace_recorder_;
/*! \brief The maximum number of tokens to rollback. */
const int MAX_ROLLBACK_TOKENS_ = 10;
};
EngineAction EngineAction::BatchJumpForward(Array<Model> models, Tokenizer tokenizer,
Optional<EventTraceRecorder> trace_recorder) {
return EngineAction(tvm::ffi::make_object<BatchJumpForwardActionObj>(
std::move(models), std::move(tokenizer), std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
@@ -0,0 +1,534 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/batch_prefill_base.h
*/
#include "batch_prefill_base.h"
#include <tvm/support/cuda/nvtx.h>
#include <numeric>
#include "../../support/json_parser.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
bool HasPrefillSpace(int num_required_pages, bool sliding_window_enabled, int new_batch_size,
int num_available_pages, int current_total_seq_len, int total_input_length,
int max_total_sequence_length) {
return num_required_pages + (!sliding_window_enabled ? new_batch_size : 0) <=
num_available_pages &&
(sliding_window_enabled ||
current_total_seq_len + total_input_length + 8 * new_batch_size <=
max_total_sequence_length);
}
BatchPrefillBaseActionObj::BatchPrefillBaseActionObj(
Array<Model> models, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs, Optional<EventTraceRecorder> trace_recorder)
: models_(std::move(models)),
engine_config_(std::move(engine_config)),
trace_recorder_(std::move(trace_recorder)) {
TVM_FFI_ICHECK_EQ(models_.size(), model_configs.size());
sliding_window_sizes_.reserve(models_.size());
for (const tvm::ffi::json::Object& model_config : model_configs) {
// "-1" means the sliding window is disabled.
sliding_window_sizes_.push_back(
json::LookupOrDefault<int64_t>(model_config, "sliding_window_size", -1));
}
kv_state_kind_ = models_[0]->GetMetadata().kv_state_kind;
}
/*!
* \brief Find one or multiple request state entries to run prefill.
* \param estate The engine state.
* \return The request entries to prefill, together with their input lengths.
*/
std::vector<BatchPrefillBaseActionObj::PrefillInput>
BatchPrefillBaseActionObj::GetRequestStateEntriesToPrefill(EngineState estate) {
// Preempt request state entries when decode cannot apply.
const std::vector<RequestStateEntry>* running_rsentries;
{
NVTXScopedRange nvtx_scope("BatchDecode getting requests");
running_rsentries = &estate->GetRunningRequestStateEntries();
if (!(running_rsentries->size() <= models_[0]->GetNumAvailablePages())) {
// Even the decode cannot be performed.
// As a result, directly return without doing prefill.
return {};
}
}
if (estate->waiting_queue.empty()) {
// No request to prefill.
return {};
}
std::vector<std::vector<PrefillInput>> prefill_inputs_for_all_models;
prefill_inputs_for_all_models.reserve(models_.size());
int num_decode_inputs = static_cast<int>(running_rsentries->size());
// We first collect the inputs that can be prefilled for each model.
// Then we make a reduction to return the maximum common inputs.
for (int i = 0; i < static_cast<int>(models_.size()); ++i) {
std::vector<PrefillInput> prefill_inputs;
// - Try to prefill pending requests.
int total_input_length = 0;
for (const RequestStateEntry& rsentry : *running_rsentries) {
total_input_length += rsentry->mstates[i]->num_tokens_for_next_decode;
}
int total_required_pages = num_decode_inputs;
int num_available_pages;
int num_running_rsentries = num_decode_inputs;
int current_total_seq_len;
{
NVTXScopedRange nvtx_scope("KV cache GetNumAvailablePages");
num_available_pages = models_[i]->GetNumAvailablePages();
}
{
NVTXScopedRange nvtx_scope("KV cache GetCurrentTotalSequenceLength");
current_total_seq_len = models_[i]->GetCurrentTotalSequenceLength();
}
int num_prefill_rsentries = 0;
for (const Request& request : estate->waiting_queue) {
NVTXScopedRange nvtx_scope("Process request " + request->id);
if (request->generation_cfg->debug_config.disagg_config.kind != DisaggRequestKind::kNone) {
continue;
}
RequestState rstate = estate->GetRequestState(request);
bool prefill_stops = false;
for (const RequestStateEntry& rsentry : rstate->entries) {
// A request state entry can be prefilled only when:
// - it has inputs, and
// - it has no parent or its parent is alive and has no remaining input.
if (rsentry->mstates[i]->inputs.empty() ||
(rsentry->parent_idx != -1 &&
(rstate->entries[rsentry->parent_idx]->status == RequestStateStatus::kPending ||
!rstate->entries[rsentry->parent_idx]->mstates[i]->inputs.empty()))) {
continue;
}
int input_length = rsentry->mstates[i]->GetInputLength();
int num_require_pages = (input_length + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
bool sliding_window_enabled = sliding_window_sizes_[i] != -1;
int num_required_pages_under_sliding_window = std::numeric_limits<int>::max();
if (sliding_window_enabled) {
// Sliding window for model i is enabled.
int max_single_request_page_requirement =
1 + (sliding_window_sizes_[i] + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
int num_total_prefilled_tokens = rsentry->mstates[i]->num_prefilled_tokens;
int parent_ptr = rsentry->parent_idx;
while (parent_ptr != -1) {
num_total_prefilled_tokens +=
rstate->entries[parent_ptr]->mstates[i]->num_prefilled_tokens;
parent_ptr = rstate->entries[parent_ptr]->parent_idx;
}
int num_pages_in_use = (std::min(num_total_prefilled_tokens, sliding_window_sizes_[i]) +
engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
num_required_pages_under_sliding_window =
max_single_request_page_requirement - num_pages_in_use;
num_require_pages = std::min(num_require_pages, num_required_pages_under_sliding_window);
TVM_FFI_ICHECK_GE(num_require_pages, 0);
}
total_input_length += input_length;
total_required_pages += num_require_pages;
// - Attempt 1. Check if the entire request state entry can fit for prefill.
bool can_prefill = false;
{
NVTXScopedRange nvtx_scope("Attempt 1");
for (int num_child_to_activate = rsentry->child_indices.size();
num_child_to_activate >= 0; --num_child_to_activate) {
while (!HasPrefillSpace(total_required_pages, sliding_window_enabled,
(num_running_rsentries + num_prefill_rsentries),
num_available_pages, current_total_seq_len, total_input_length,
engine_config_->max_total_sequence_length)) {
if (!estate->prefix_cache->TryFreeMemory()) break;
// Update number of available pages after memory free.
num_available_pages = models_[i]->GetNumAvailablePages();
current_total_seq_len = models_[i]->GetCurrentTotalSequenceLength();
}
if (CanPrefill(estate, num_prefill_rsentries + 1 + num_child_to_activate,
total_input_length, total_required_pages, num_available_pages,
current_total_seq_len, num_running_rsentries, kv_state_kind_,
sliding_window_enabled)) {
prefill_inputs.push_back(
{rsentry, input_length, num_child_to_activate, /*is_decode=*/false});
num_prefill_rsentries += 1 + num_child_to_activate;
can_prefill = true;
break;
}
}
}
if (can_prefill) {
continue;
}
total_input_length -= input_length;
total_required_pages -= num_require_pages;
// - Attempt 2. Check if the request state entry can partially fit by input chunking.
TVM_FFI_ICHECK_LE(total_input_length, engine_config_->prefill_chunk_size);
if (engine_config_->prefill_chunk_size - total_input_length >= input_length ||
engine_config_->prefill_chunk_size == total_input_length) {
// 1. If the input length can fit the remaining prefill chunk size,
// it means the failure of attempt 1 is not because of the input
// length being too long, and thus chunking does not help.
// 2. If the total input length already reaches the prefill chunk size,
// the current request state entry will not be able to be processed.
// So we can safely return in either case.
prefill_stops = true;
break;
}
input_length = engine_config_->prefill_chunk_size - total_input_length;
num_require_pages = (input_length + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
if (sliding_window_enabled) {
// Sliding window for model i is enabled.
num_require_pages = std::min(num_require_pages, num_required_pages_under_sliding_window);
TVM_FFI_ICHECK_GE(num_require_pages, 0);
}
{
NVTXScopedRange nvtx_scope("Attempt 2");
total_input_length += input_length;
total_required_pages += num_require_pages;
if (CanPrefill(estate, num_prefill_rsentries + 1, total_input_length,
total_required_pages, num_available_pages, current_total_seq_len,
num_running_rsentries, kv_state_kind_, sliding_window_enabled)) {
prefill_inputs.push_back({rsentry, input_length, 0, /*is_decode=*/false});
}
}
// - Prefill stops here.
prefill_stops = true;
break;
}
if (prefill_stops) {
break;
}
}
prefill_inputs_for_all_models.push_back(prefill_inputs);
}
// Reduce over the prefill inputs of all models.
TVM_FFI_ICHECK(!prefill_inputs_for_all_models.empty());
int num_prefill_inputs = prefill_inputs_for_all_models[0].size();
for (int i = 1; i < static_cast<int>(prefill_inputs_for_all_models.size()); ++i) {
num_prefill_inputs =
std::min(num_prefill_inputs, static_cast<int>(prefill_inputs_for_all_models[i].size()));
}
if (num_prefill_inputs == 0) {
return {};
}
// Add the decode requests to the prefill inputs if prefill mode is hybrid.
std::vector<PrefillInput> prefill_inputs(prefill_inputs_for_all_models[0].begin(),
prefill_inputs_for_all_models[0].end());
if (engine_config_->prefill_mode == PrefillMode::kHybrid) {
prefill_inputs.reserve(num_decode_inputs + num_prefill_inputs);
for (const RequestStateEntry& rsentry : *running_rsentries) {
prefill_inputs.push_back(
{rsentry, rsentry->mstates[0]->num_tokens_for_next_decode, 0, /*is_decode=*/true});
}
}
{
NVTXScopedRange nvtx_scope("reduction");
for (int i = 1; i < static_cast<int>(prefill_inputs_for_all_models.size()); ++i) {
// Prefill input lengths except the last one are supposed to be the same for all models.
for (int j = 0; j < num_prefill_inputs - 1; ++j) {
TVM_FFI_ICHECK(
prefill_inputs_for_all_models[i][j].rsentry.same_as(prefill_inputs[j].rsentry));
TVM_FFI_ICHECK_EQ(prefill_inputs_for_all_models[i][j].max_prefill_length,
prefill_inputs[j].max_prefill_length);
prefill_inputs[j].num_child_to_activate =
std::min(prefill_inputs[j].num_child_to_activate,
prefill_inputs_for_all_models[i][j].num_child_to_activate);
}
// The input length of the last input is the minimum among all models.
TVM_FFI_ICHECK(prefill_inputs_for_all_models[i][num_prefill_inputs - 1].rsentry.same_as(
prefill_inputs[num_prefill_inputs - 1].rsentry));
prefill_inputs[num_prefill_inputs - 1].max_prefill_length =
std::min(prefill_inputs[num_prefill_inputs - 1].max_prefill_length,
prefill_inputs_for_all_models[i][num_prefill_inputs - 1].max_prefill_length);
prefill_inputs[num_prefill_inputs - 1].num_child_to_activate =
std::min(prefill_inputs[num_prefill_inputs - 1].num_child_to_activate,
prefill_inputs_for_all_models[i][num_prefill_inputs - 1].num_child_to_activate);
}
}
return prefill_inputs;
}
bool BatchPrefillBaseActionObj::CanPrefill(EngineState estate, int num_prefill_rsentries,
int total_input_length, int num_required_pages,
int num_available_pages, int current_total_seq_len,
int num_running_rsentries, KVStateKind kv_state_kind,
bool sliding_window_enabled) {
TVM_FFI_ICHECK_LE(num_running_rsentries, engine_config_->max_num_sequence);
// For pure RNN State, it can prefill as long as it can be instantiated.
// Hybrid uses KVCache for capacity (PagedKVCache is the constraining factor).
if (kv_state_kind == KVStateKind::kRNNState || kv_state_kind == KVStateKind::kNone) {
return true;
}
// No exceeding of the maximum allowed requests that can
// run simultaneously.
int spec_factor = engine_config_->speculative_mode != SpeculativeMode::kDisable
? (estate->spec_draft_length + 1)
: 1;
if ((num_running_rsentries + num_prefill_rsentries) * spec_factor >
std::min(static_cast<int64_t>(engine_config_->max_num_sequence),
engine_config_->prefill_chunk_size)) {
return false;
}
// NOTE: The conditions are heuristic and can be revised.
// Cond 1: total input length <= prefill chunk size.
// Cond 2: at least one decode can be performed after prefill.
// Cond 3: number of total tokens after 8 times of decode does not
// exceed the limit, where 8 is a watermark number can
// be configured and adjusted in the future.
return total_input_length <= engine_config_->prefill_chunk_size &&
HasPrefillSpace(num_required_pages, sliding_window_enabled,
(num_running_rsentries + num_prefill_rsentries), num_available_pages,
current_total_seq_len, total_input_length,
engine_config_->max_total_sequence_length);
}
/*!
* \brief Chunk the input of the given RequestModelState for prefill
* with regard to the provided maximum allowed prefill length.
* Return the list of input for prefill and the total prefill length.
* The `inputs` field of the given `mstate` will be mutated to exclude
* the returned input.
* \param mstate The RequestModelState whose input data is to be chunked.
* \param max_prefill_length The maximum allowed prefill length for the mstate.
* \return The list of input for prefill and the total prefill length.
*/
std::pair<Array<Data>, int> BatchPrefillBaseActionObj::ChunkPrefillInputData(
const RequestModelState& mstate, int max_prefill_length) {
if (mstate->inputs.empty()) {
// If the request is a hybrid decode request
TVM_FFI_ICHECK(mstate->num_tokens_for_next_decode > 0);
int num_tokens = mstate->num_tokens_for_next_decode;
mstate->num_tokens_for_next_decode = 0;
std::vector<int32_t> decode_tokens;
decode_tokens.reserve(num_tokens);
for (auto begin = mstate->committed_tokens.end() - num_tokens;
begin != mstate->committed_tokens.end(); ++begin) {
decode_tokens.push_back(begin->GetTokenId());
}
return {{TokenData(decode_tokens)}, num_tokens};
}
TVM_FFI_ICHECK(!mstate->inputs.empty());
std::vector<Data> inputs;
int cum_input_length = 0;
inputs.reserve(mstate->inputs.size());
for (int i = 0; i < static_cast<int>(mstate->inputs.size()); ++i) {
inputs.push_back(mstate->inputs[i]);
int input_length = mstate->inputs[i]->GetLength();
cum_input_length += input_length;
// Case 0. the cumulative input length does not reach the maximum prefill length.
if (cum_input_length < max_prefill_length) {
continue;
}
// Case 1. the cumulative input length equals the maximum prefill length.
if (cum_input_length == max_prefill_length) {
if (i == static_cast<int>(mstate->inputs.size()) - 1) {
// - If `i` is the last input, we just copy and reset `mstate->inputs`.
mstate->inputs.clear();
} else {
// - Otherwise, set the new input array.
mstate->inputs = Array<Data>{mstate->inputs.begin() + i + 1, mstate->inputs.end()};
}
return {inputs, cum_input_length};
}
// Case 2. cum_input_length > max_prefill_length
// The input `i` itself needs chunking if it is TokenData,
// or otherwise it cannot be chunked.
Data input = mstate->inputs[i];
inputs.pop_back();
cum_input_length -= input_length;
const auto* token_input = input.as<TokenDataNode>();
if (token_input == nullptr) {
// Cannot chunk the input.
if (i != 0) {
mstate->inputs = Array<Data>{mstate->inputs.begin() + i, mstate->inputs.end()};
}
return {inputs, cum_input_length};
}
// Split the token data into two parts.
// Return the first part for prefill, and keep the second part.
int chunked_input_length = max_prefill_length - cum_input_length;
TVM_FFI_ICHECK_GT(input_length, chunked_input_length);
TokenData chunked_input(Shape{token_input->token_ids.begin(),
token_input->token_ids.begin() + chunked_input_length});
TokenData remaining_input(
Shape{token_input->token_ids.begin() + chunked_input_length, token_input->token_ids.end()});
inputs.push_back(chunked_input);
cum_input_length += chunked_input_length;
std::vector<Data> remaining_inputs{mstate->inputs.begin() + i + 1, mstate->inputs.end()};
remaining_inputs.insert(remaining_inputs.begin(), remaining_input);
mstate->inputs = remaining_inputs;
return {inputs, cum_input_length};
}
TVM_FFI_ICHECK(false) << "Cannot reach here";
}
void BatchPrefillBaseActionObj::UpdateRequestToAlive(
const std::vector<BatchPrefillBaseActionObj::PrefillInput>& prefill_inputs,
const EngineState& estate, Array<String>* request_ids,
std::vector<RequestState>* rstates_of_entries,
std::vector<RequestStateStatus>* status_before_prefill) {
int num_rsentries = prefill_inputs.size();
request_ids->reserve(num_rsentries);
rstates_of_entries->reserve(num_rsentries);
status_before_prefill->reserve(num_rsentries);
for (const PrefillInput& prefill_input : prefill_inputs) {
const RequestStateEntry& rsentry = prefill_input.rsentry;
const Request& request = rsentry->request;
RequestState request_rstate = estate->GetRequestState(request);
request_ids->push_back(request->id);
status_before_prefill->push_back(rsentry->status);
rsentry->status = RequestStateStatus::kAlive;
if (status_before_prefill->back() == RequestStateStatus::kPending) {
// - Add the request to running queue if the request state
// status was pending and all its request states were pending.
bool alive_state_existed = false;
for (const RequestStateEntry& rsentry_ : request_rstate->entries) {
if (rsentry_->status == RequestStateStatus::kAlive && !rsentry_.same_as(rsentry)) {
alive_state_existed = true;
}
}
if (!alive_state_existed) {
estate->running_queue.push_back(request);
}
}
rstates_of_entries->push_back(std::move(request_rstate));
}
}
std::vector<Request> BatchPrefillBaseActionObj::RemoveProcessedRequests(
const std::vector<BatchPrefillBaseActionObj::PrefillInput>& prefill_inputs,
const EngineState& estate, const std::vector<RequestState>& rstates_of_entries) {
// - Remove the request from waiting queue if all its request states
// are now alive and have no remaining chunked inputs.
std::vector<Request> processed_requests;
int num_rsentries = prefill_inputs.size();
processed_requests.reserve(num_rsentries);
std::unordered_set<const RequestNode*> dedup_map;
for (int i = 0; i < num_rsentries; ++i) {
const RequestStateEntry& rsentry = prefill_inputs[i].rsentry;
if (dedup_map.find(rsentry->request.operator->()) != dedup_map.end()) {
continue;
}
dedup_map.insert(rsentry->request.operator->());
processed_requests.push_back(rsentry->request);
bool pending_state_exists = false;
for (const RequestStateEntry& rsentry_ : rstates_of_entries[i]->entries) {
if (rsentry_->status == RequestStateStatus::kPending ||
!rsentry_->mstates[0]->inputs.empty()) {
pending_state_exists = true;
break;
}
}
if (!pending_state_exists &&
std::find(estate->waiting_queue.begin(), estate->waiting_queue.end(), rsentry->request) !=
estate->waiting_queue.end()) {
auto it =
std::find(estate->waiting_queue.begin(), estate->waiting_queue.end(), rsentry->request);
if (it != estate->waiting_queue.end()) {
estate->waiting_queue.erase(it);
}
}
}
return processed_requests;
}
void BatchPrefillBaseActionObj::UpdateRequestStateEntriesWithSampleResults(
const std::vector<RequestStateEntry>& rsentries_for_sample,
const std::vector<bool>& rsentry_activated, const std::vector<SampleResult>& sample_results) {
auto tnow = std::chrono::high_resolution_clock::now();
for (int i = 0; i < static_cast<int>(rsentries_for_sample.size()); ++i) {
// If the request is a hybrid decode request
if (rsentries_for_sample[i]->status == RequestStateStatus::kAlive &&
rsentries_for_sample[i]->child_indices.empty() &&
rsentries_for_sample[i]->mstates[0]->inputs.empty()) {
for (const RequestModelState& mstate : rsentries_for_sample[i]->mstates) {
TVM_FFI_ICHECK(!mstate->require_retokenization_in_next_decode);
mstate->CommitToken(sample_results[i]);
// live update the output metrics
rsentries_for_sample[i]->rstate->metrics.completion_tokens += 1;
rsentries_for_sample[i]->rstate->metrics.prefill_end_time_point = tnow;
}
continue;
}
// Update all model states of the request state entry.
for (const RequestModelState& mstate : rsentries_for_sample[i]->mstates) {
mstate->CommitToken(sample_results[i]);
if (!rsentry_activated[i]) {
// When the child rsentry is not activated,
// add the sampled token as an input of the mstate for prefill.
mstate->inputs.push_back(TokenData(std::vector<int64_t>{sample_results[i].GetTokenId()}));
}
}
// prefill has finished
if (rsentries_for_sample[i]->mstates[0]->committed_tokens.size() == 1) {
TVM_FFI_ICHECK(rsentries_for_sample[i]->rstate != nullptr);
rsentries_for_sample[i]->rstate->metrics.prefill_end_time_point = tnow;
}
}
}
std::vector<int32_t> BatchPrefillBaseActionObj::GetConcatPrefillInputData(
const RequestModelState& mstate) {
std::vector<int32_t> tokens;
for (Data data : mstate->inputs) {
if (const TokenDataNode* token_data = data.as<TokenDataNode>()) {
tokens.reserve(tokens.size() + token_data->GetLength());
tokens.insert(tokens.end(), token_data->token_ids.begin(), token_data->token_ids.end());
} else {
return {};
}
}
return tokens;
}
void BatchPrefillBaseActionObj::PopPrefillInputData(const RequestModelState& mstate,
size_t num_tokens) {
while (mstate->inputs[0]->GetLength() <= num_tokens) {
num_tokens -= mstate->inputs[0]->GetLength();
mstate->inputs.erase(mstate->inputs.begin());
}
if (num_tokens) {
const TokenDataNode* token_data = mstate->inputs[0].as<TokenDataNode>();
std::vector<int32_t> tokens;
tokens.reserve(token_data->GetLength() - num_tokens);
tokens.insert(tokens.begin(), token_data->token_ids.begin() + num_tokens,
token_data->token_ids.end());
mstate->inputs.erase(mstate->inputs.begin());
mstate->inputs.insert(mstate->inputs.begin(), TokenData(tokens));
}
}
} // namespace serve
} // namespace llm
} // namespace mlc
@@ -0,0 +1,144 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/batch_prefill_base.h
*/
#include "../config.h"
#include "../model.h"
#include "action.h"
#include "action_commons.h"
namespace mlc {
namespace llm {
namespace serve {
/*!
* \brief The base action of that prefills requests in the `waiting_queue` of
* the engine state.
*/
class BatchPrefillBaseActionObj : public EngineActionObj {
protected:
/*! \brief The class of request state entry and its maximum allowed length for prefill. */
struct PrefillInput {
RequestStateEntry rsentry;
int max_prefill_length = 0;
int num_child_to_activate = 0;
bool is_decode = false;
};
BatchPrefillBaseActionObj(Array<Model> models, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Find one or multiple request state entries to run prefill.
* \param estate The engine state.
* \return The request entries to prefill, together with their input lengths.
*/
std::vector<PrefillInput> GetRequestStateEntriesToPrefill(EngineState estate);
/*! \brief Check if the input requests can be prefilled under conditions. */
bool CanPrefill(EngineState estate, int num_prefill_rsentries, int total_input_length,
int num_required_pages, int num_available_pages, int current_total_seq_len,
int num_running_rsentries, KVStateKind kv_state_kind,
bool sliding_window_enabled);
/*!
* \brief Chunk the input of the given RequestModelState for prefill
* with regard to the provided maximum allowed prefill length.
* Return the list of input for prefill and the total prefill length.
* The `inputs` field of the given `mstate` will be mutated to exclude
* the returned input.
* \param mstate The RequestModelState whose input data is to be chunked.
* \param max_prefill_length The maximum allowed prefill length for the mstate.
* \return The list of input for prefill and the total prefill length.
*/
std::pair<Array<Data>, int> ChunkPrefillInputData(const RequestModelState& mstate,
int max_prefill_length);
/*!
* \brief Update status of request states from pending to alive and collect request state entries
* from the prefill input.
* \param prefill_inputs The prefill input.
* \param estate The engine state.
* \param[out] request_ids The array to store the request ids of the request state entries.
* \param[out] rstates_of_entries The vector to store the request state entries.
* \param[out] status_before_prefill The vector to store the status of the request state entries
* before prefill.
*/
void UpdateRequestToAlive(const std::vector<PrefillInput>& prefill_inputs,
const EngineState& estate, Array<String>* request_ids,
std::vector<RequestState>* rstates_of_entries,
std::vector<RequestStateStatus>* status_before_prefill);
/*!
* \brief Remove the request from waiting queue if all its request states are now alive and have
* no remaining chunked inputs.
* \param prefill_inputs The prefill input.
* \param estate The engine state.
* \param rstates_of_entries The request state entries for each prefill input.
* \return The processed requests.
*/
std::vector<Request> RemoveProcessedRequests(const std::vector<PrefillInput>& prefill_inputs,
const EngineState& estate,
const std::vector<RequestState>& rstates_of_entries);
/*!
* \brief Update the committed tokens of states. If a request is first-time prefilled, set the
* prefill finish time.
* \param rsentries_for_sample The request state entries for sample.
* \param rsentry_activated The activation status of the request state entries.
* \param sample_results The sample results.
*/
void UpdateRequestStateEntriesWithSampleResults(
const std::vector<RequestStateEntry>& rsentries_for_sample,
const std::vector<bool>& rsentry_activated, const std::vector<SampleResult>& sample_results);
/*!
* \brief Get the concatenated Shape of RequestModelState input data, return empty Shape if
* there is untokenized data.
* \param mstate The RequestModelState whose input data is to be concatenated.
* \return The concatenate Shape.
*/
std::vector<int32_t> GetConcatPrefillInputData(const RequestModelState& mstate);
/*!
* \brief Pop the prefix tokens of the RequestModelState input data array.
* \param mstate The RequestModelState to be popped.
* \param num_tokens The number of prefix tokens to be popped.
*/
void PopPrefillInputData(const RequestModelState& mstate, size_t num_tokens);
/*!
* \brief Match the request state entry with prefix cache, to skip prefilling common prefix
* tokens. If the request state entry is not added to KVCache yet, this method will add/fork the
* request in the KVCache, depending on the matching result from prefix cache.
* \param estate The engine state.
* \param[in, out] input The prefill input to be matched and updated.
* \return The matched length in prefix cache.
*/
virtual int MatchPrefixCache(EngineState estate, PrefillInput* input) = 0;
/*! \brief The models to run prefill in. */
Array<Model> models_;
/*! \brief The engine config. */
EngineConfig engine_config_;
/*! \brief The KV state kind. */
KVStateKind kv_state_kind_;
/*! \brief The sliding window size of each model. */
std::vector<int> sliding_window_sizes_;
/*! \brief Event trace recorder. */
Optional<EventTraceRecorder> trace_recorder_;
};
/*!
* \brief A utility function to check whether there is enough spare space in
* KV cache for the number of required pages and total input length.
*/
bool HasPrefillSpace(int num_required_pages, bool sliding_window_enabled, int new_batch_size,
int num_available_pages, int current_total_seq_len, int total_input_length,
int max_total_sequence_length);
} // namespace serve
} // namespace llm
} // namespace mlc
+381
View File
@@ -0,0 +1,381 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/batch_verify.cc
*/
#include <cmath>
#include <exception>
#include <numeric>
#include "../../support/random.h"
#include "../config.h"
#include "../model.h"
#include "../sampler/sampler.h"
#include "action.h"
#include "action_commons.h"
namespace mlc {
namespace llm {
namespace serve {
/*!
* \brief The action that runs verification for requests in the
* `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
*/
class BatchVerifyActionObj : public EngineActionObj {
public:
explicit BatchVerifyActionObj(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler, std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder)
: models_(std::move(models)),
logit_processor_(std::move(logit_processor)),
sampler_(std::move(sampler)),
model_workspaces_(std::move(model_workspaces)),
draft_token_workspace_manager_(std::move(draft_token_workspace_manager)),
engine_config_(std::move(engine_config)),
trace_recorder_(std::move(trace_recorder)),
rng_(RandomGenerator::GetInstance()) {}
Array<Request> Step(EngineState estate) final {
// - Only run spec decode when there are two models (llm+ssm) and >=1 running requests.
if (models_.size() != 2 || estate->running_queue.empty()) {
return {};
}
const auto& [rsentries, verify_lengths, total_verify_length] = GetDraftsToVerify(estate);
TVM_FFI_ICHECK_EQ(rsentries.size(), verify_lengths.size());
if (rsentries.empty()) {
return {};
}
auto tstart = std::chrono::high_resolution_clock::now();
int num_rsentries = rsentries.size();
Array<String> request_ids =
rsentries.Map([](const RequestStateEntry& rstate) { return rstate->request->id; });
// - Get embedding and run verify.
std::vector<int64_t> request_internal_ids;
std::vector<int32_t> all_tokens_to_verify;
Array<RequestModelState> verify_request_mstates;
Array<RequestModelState> draft_request_mstates;
Array<GenerationConfig> generation_cfg;
Array<GenerationConfig> generation_cfg_for_top_p_norm;
std::vector<RandomGenerator*> rngs;
std::vector<std::vector<SampleResult>> draft_output_tokens;
std::vector<int64_t> token_tree_parent_ptr;
std::vector<std::vector<int>> draft_token_indices;
token_tree_parent_ptr.reserve(total_verify_length);
request_internal_ids.reserve(num_rsentries);
all_tokens_to_verify.reserve(total_verify_length);
draft_token_indices.reserve(num_rsentries);
verify_request_mstates.reserve(num_rsentries);
draft_request_mstates.reserve(num_rsentries);
rngs.reserve(num_rsentries);
generation_cfg.reserve(num_rsentries);
generation_cfg_for_top_p_norm.reserve(total_verify_length);
draft_output_tokens.reserve(num_rsentries);
draft_token_slots_.clear();
for (int i = 0; i < num_rsentries; ++i) {
RequestModelState verify_mstate = rsentries[i]->mstates[verify_model_id_];
RequestModelState draft_mstate = rsentries[i]->mstates[draft_model_id_];
request_internal_ids.push_back(verify_mstate->internal_id);
TVM_FFI_ICHECK(!verify_lengths.empty());
TVM_FFI_ICHECK_EQ(verify_lengths[i], draft_mstate->draft_output_tokens.size() + 1);
TVM_FFI_ICHECK_EQ(verify_lengths[i], draft_mstate->draft_token_slots.size() + 1);
// the last committed token + all the draft tokens.
draft_token_slots_.push_back(0); // placeholder for the last committed token
all_tokens_to_verify.push_back(draft_mstate->committed_tokens.back().GetTokenId());
token_tree_parent_ptr.push_back(-1);
generation_cfg_for_top_p_norm.push_back(rsentries[i]->request->generation_cfg);
std::vector<int> cur_draft_token_indices;
cur_draft_token_indices.resize(draft_mstate->draft_output_tokens.size() + 1);
std::iota(cur_draft_token_indices.begin(), cur_draft_token_indices.end(), -1);
for (int j = 0; j < static_cast<int>(draft_mstate->draft_output_tokens.size()); ++j) {
all_tokens_to_verify.push_back(draft_mstate->draft_output_tokens[j].GetTokenId());
draft_token_slots_.push_back(draft_mstate->draft_token_slots[j]);
token_tree_parent_ptr.push_back(draft_mstate->draft_token_parent_idx[j] + 1);
generation_cfg_for_top_p_norm.push_back(rsentries[i]->request->generation_cfg);
}
draft_token_indices.emplace_back(std::move(cur_draft_token_indices));
verify_request_mstates.push_back(verify_mstate);
draft_request_mstates.push_back(draft_mstate);
generation_cfg.push_back(rsentries[i]->request->generation_cfg);
rngs.push_back(&rsentries[i]->rng);
draft_output_tokens.push_back(draft_mstate->draft_output_tokens);
}
Tensor draft_probs_on_device = models_[draft_model_id_]->GatherDraftProbs(
model_workspaces_[verify_model_id_].draft_probs_storage, draft_token_slots_,
&model_workspaces_[verify_model_id_].draft_probs);
RECORD_EVENT(trace_recorder_, request_ids, "start verify embedding");
ObjectRef embeddings = models_[verify_model_id_]->TokenEmbed(
{Shape{all_tokens_to_verify.begin(), all_tokens_to_verify.end()}});
RECORD_EVENT(trace_recorder_, request_ids, "finish verify embedding");
RECORD_EVENT(trace_recorder_, request_ids, "start verify");
Tensor logits = models_[verify_model_id_]->BatchVerify(embeddings, request_internal_ids,
verify_lengths, token_tree_parent_ptr);
RECORD_EVENT(trace_recorder_, request_ids, "finish verify");
TVM_FFI_ICHECK_EQ(logits->ndim, 3);
TVM_FFI_ICHECK_EQ(logits->shape[0], 1);
TVM_FFI_ICHECK_EQ(logits->shape[1], total_verify_length);
// - Update logits.
std::vector<int> cum_verify_lengths = {0};
cum_verify_lengths.reserve(num_rsentries + 1);
for (int i = 0; i < num_rsentries; ++i) {
cum_verify_lengths.push_back(cum_verify_lengths.back() + verify_lengths[i]);
}
logits = logits.CreateView({total_verify_length, logits->shape[2]}, logits->dtype);
logit_processor_->InplaceUpdateLogits(logits, generation_cfg, verify_request_mstates,
request_ids, &cum_verify_lengths, &draft_request_mstates,
&draft_token_indices);
// - Compute probability distributions.
Tensor probs_on_device = logit_processor_->ComputeProbsFromLogits(
logits, generation_cfg, request_ids, &cum_verify_lengths);
// - Commit the prefix cache changes from previous round of action.
// Note: we commit prefix cache changes here to overlap this commit with the GPU execution.
estate->prefix_cache->CommitSequenceExtention();
// Fill range [0, total_verify_length) into `sample_indices`.
std::vector<int> sample_indices(total_verify_length);
std::iota(sample_indices.begin(), sample_indices.end(), 0);
Tensor renormalized_probs = sampler_->BatchRenormalizeProbsByTopP(
probs_on_device, sample_indices, request_ids, generation_cfg_for_top_p_norm);
auto [sample_results_arr, last_accepted_tree_node_verify_model] =
sampler_->BatchVerifyDraftTokensWithProbAfterTopP(
renormalized_probs, request_ids, cum_verify_lengths, generation_cfg, rngs,
draft_output_tokens, token_tree_parent_ptr, draft_probs_on_device);
TVM_FFI_ICHECK_EQ(sample_results_arr.size(), num_rsentries);
// We collect the requests whose drafts are fully accepted.
// When a request's draft is fully accepted, there is an extra token proposed
// by the draft model but not added into the draft model's KV cache.
// In this case, an additional batch decode step is needed for these requests.
std::vector<int64_t> fully_accepted_rsentries;
std::vector<int64_t> verify_model_seq_internal_ids;
std::vector<int64_t> draft_model_seq_internal_ids;
fully_accepted_rsentries.reserve(num_rsentries);
verify_model_seq_internal_ids.reserve(num_rsentries);
draft_model_seq_internal_ids.reserve(num_rsentries);
// The index of the last accepted tree node in the draft model. This is different from the
// last accepted tree node in the verify model because the first round of draft does not
// use tree attention.
std::vector<int64_t> last_accepted_tree_node_draft_model;
last_accepted_tree_node_draft_model.reserve(num_rsentries);
for (int i = 0; i < num_rsentries; ++i) {
const std::vector<SampleResult>& sample_results = sample_results_arr[i];
int accept_length = sample_results.size();
for (SampleResult sample_result : sample_results) {
rsentries[i]->mstates[verify_model_id_]->CommitToken(sample_result);
rsentries[i]->mstates[draft_model_id_]->CommitToken(sample_result);
}
// Metrics update
// live update the output metrics
rsentries[i]->rstate->metrics.completion_tokens += accept_length;
estate->metrics.spec_decode.Update(cum_verify_lengths[i + 1] - cum_verify_lengths[i],
accept_length);
if (engine_config_->spec_tree_width == 1) {
// The roll back is needed for the chain draft case.
int rollback_length =
std::max(cum_verify_lengths[i + 1] - cum_verify_lengths[i] - accept_length, 0);
if (rollback_length > 0) {
// The last accepted token is not yet added into the draft model.
// Therefore, the rollback length for the draft model is one less.
models_[draft_model_id_]->PopNFromKVCache(
rsentries[i]->mstates[draft_model_id_]->internal_id, rollback_length - 1);
}
}
// Commit accepted tokens to the "verify_model", rollback kv cache
// in the "draft_model".
// NOTE: when number of small models is more than 1 (in the future),
// it is possible to re-compute prefill for the small models.
verify_model_seq_internal_ids.push_back(rsentries[i]->mstates[verify_model_id_]->internal_id);
draft_model_seq_internal_ids.push_back(rsentries[i]->mstates[draft_model_id_]->internal_id);
int last_accepted = last_accepted_tree_node_verify_model[i] -
1; // minus one to get the index in the draft tokens
if (last_accepted >= 0 &&
rsentries[i]->mstates[draft_model_id_]->draft_token_first_child_idx[last_accepted] ==
-1) { // minus one to get the index in the draft tokens
// is leaf node, fully accepted
last_accepted_tree_node_draft_model.push_back(
rsentries[i]->mstates[draft_model_id_]->draft_token_parent_idx[last_accepted]);
fully_accepted_rsentries.push_back(i);
} else {
last_accepted_tree_node_draft_model.push_back(last_accepted);
}
}
models_[verify_model_id_]->CommitAcceptedTokenTreeNodesToKVCache(
verify_model_seq_internal_ids,
std::vector<int64_t>{last_accepted_tree_node_verify_model.begin(),
last_accepted_tree_node_verify_model.end()});
if (engine_config_->spec_tree_width > 1) {
models_[draft_model_id_]->CommitAcceptedTokenTreeNodesToKVCache(
draft_model_seq_internal_ids, last_accepted_tree_node_draft_model);
}
if (!fully_accepted_rsentries.empty()) {
// - Run a step of batch decode for requests whose drafts are fully accepted.
// When a request's draft is fully accepted, there is an extra token proposed
// by the draft model but not added into the draft model's KV cache.
// In this case, an additional batch decode step is needed for these requests.
std::vector<int> input_tokens;
std::vector<int64_t> fully_accepted_request_internal_ids;
input_tokens.reserve(fully_accepted_rsentries.size());
fully_accepted_request_internal_ids.reserve(fully_accepted_rsentries.size());
for (int rsentry_id : fully_accepted_rsentries) {
int num_committed_tokens =
rsentries[rsentry_id]->mstates[verify_model_id_]->committed_tokens.size();
// When a request's draft is fully accepted, an additional new token is sampled.
// So the token needed to fill in the draft model is the committed_token[-2].
TVM_FFI_ICHECK_GE(num_committed_tokens, 2);
input_tokens.push_back(rsentries[rsentry_id]
->mstates[verify_model_id_]
->committed_tokens[num_committed_tokens - 2]
.GetTokenId());
fully_accepted_request_internal_ids.push_back(
rsentries[rsentry_id]->mstates[draft_model_id_]->internal_id);
}
// - Compute embeddings.
ObjectRef embeddings =
models_[draft_model_id_]->TokenEmbed({Shape{input_tokens.begin(), input_tokens.end()}});
// - Invoke model decode.
Tensor logits =
models_[draft_model_id_]->BatchDecode(embeddings, fully_accepted_request_internal_ids);
// - We explicitly synchronize to avoid the input tokens getting overriden in the
// next runs of BatchDecode.
// This is because we do not do sample for this round of batch decode.
DeviceAPI::Get(logits->device)->StreamSync(logits->device, nullptr);
}
// clear the draft model state entries
for (int i = 0; i < num_rsentries; ++i) {
rsentries[i]->mstates[draft_model_id_]->RemoveAllDraftTokens(&draft_token_slots_);
draft_token_workspace_manager_->FreeSlots(draft_token_slots_);
// reset num_tokens_for_next_decode to 1
rsentries[i]->mstates[verify_model_id_]->num_tokens_for_next_decode = 1;
rsentries[i]->mstates[draft_model_id_]->num_tokens_for_next_decode = 1;
}
auto tend = std::chrono::high_resolution_clock::now();
double elapsed_time = static_cast<double>((tend - tstart).count()) / 1e9;
estate->metrics.engine_decode_time_sum += elapsed_time;
estate->metrics.UpdateVerifyTimeByBatchSize(total_verify_length, elapsed_time);
return estate->running_queue;
}
private:
struct DraftRequestStateEntries {
/*! \brief The request state entries to verify. */
Array<RequestStateEntry> draft_rsentries;
/*! \brief The length to verify for each request state. */
std::vector<int> verify_lengths;
/*! \brief The total draft length. */
int total_verify_length;
};
/*!
* \brief Decide whether to run verify for the draft of each request.
* \param estate The engine state.
* \return The drafts to verify, together with their respective
* state and input length.
*/
DraftRequestStateEntries GetDraftsToVerify(EngineState estate) {
std::vector<int> verify_lengths;
int total_verify_length = 0;
int total_required_pages = 0;
int num_available_pages = models_[verify_model_id_]->GetNumAvailablePages();
// Preempt the request state entries that cannot fit the large model for verification.
std::vector<RequestStateEntry> init_running_rsentries = estate->GetRunningRequestStateEntries();
std::vector<int> num_page_requirement;
num_page_requirement.reserve(init_running_rsentries.size());
std::vector<RequestStateEntry> running_rsentries;
running_rsentries.reserve(init_running_rsentries.size());
for (const RequestStateEntry& rsentry : init_running_rsentries) {
int draft_length = rsentry->mstates[draft_model_id_]->draft_output_tokens.size();
if (draft_length == 0) {
continue;
}
running_rsentries.push_back(rsentry);
int num_require_pages = (draft_length + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
verify_lengths.push_back(draft_length + 1);
num_page_requirement.push_back(num_require_pages);
total_verify_length += draft_length + 1;
total_required_pages += num_require_pages;
}
while (!CanVerify(total_required_pages)) {
if (estate->prefix_cache->TryFreeMemory()) continue;
RequestStateEntry preempted = PreemptLastRunningRequestStateEntry(
estate, models_, draft_token_workspace_manager_, trace_recorder_);
if (preempted.same_as(running_rsentries.back())) {
total_verify_length -= verify_lengths.back();
total_required_pages -= num_page_requirement.back();
verify_lengths.pop_back();
num_page_requirement.pop_back();
running_rsentries.pop_back();
}
}
TVM_FFI_ICHECK_LE(total_verify_length,
std::min(static_cast<int64_t>(engine_config_->max_num_sequence),
engine_config_->prefill_chunk_size))
<< total_verify_length << " " << engine_config_->max_num_sequence;
return {running_rsentries, verify_lengths, total_verify_length};
}
bool CanVerify(int num_required_pages) {
int num_available_pages = models_[0]->GetNumAvailablePages();
return num_required_pages <= num_available_pages;
}
/*!
* \brief The model to run decode in. When there are multiple
* models, the `Step` function of the created action will not take effect.
*/
Array<Model> models_;
/*! \brief The logit processor. */
LogitProcessor logit_processor_;
/*! \brief The sampler to sample new tokens. */
Sampler sampler_;
/*! \brief The model workspaces. */
std::vector<ModelWorkspace> model_workspaces_;
/*! \brief The draft token workspace manager. */
DraftTokenWorkspaceManager draft_token_workspace_manager_;
/*! \brief The engine config. */
EngineConfig engine_config_;
/*! \brief Event trace recorder. */
Optional<EventTraceRecorder> trace_recorder_;
/*! \brief Random number generator. */
RandomGenerator& rng_;
/*! \brief The ids of verify/draft models. */
const int verify_model_id_ = 0;
const int draft_model_id_ = 1;
const float eps_ = 1e-5;
/*! \brief Temporary buffer to store the slots of the current draft tokens */
std::vector<int> draft_token_slots_;
};
EngineAction EngineAction::BatchVerify(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder) {
return EngineAction(tvm::ffi::make_object<BatchVerifyActionObj>(
std::move(models), std::move(logit_processor), std::move(sampler),
std::move(model_workspaces), std::move(draft_token_workspace_manager),
std::move(engine_config), std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
@@ -0,0 +1,447 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/new_request_prefill.cc
*/
#include <tvm/support/cuda/nvtx.h>
#include <optional>
#include "../../support/utils.h"
#include "../sampler/sampler.h"
#include "batch_prefill_base.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
/*!
* \brief The action that runs prefill preparation in disaggregation system.
* It picks a new request, reserve its KV data locations, and returns the
* KV data locations and the matched prefix length in prefix cache.
*/
class DisaggPrepareReceiveActionObj : public BatchPrefillBaseActionObj {
public:
explicit DisaggPrepareReceiveActionObj(Array<Model> models, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder,
FRequestStreamCallback request_stream_callback)
: BatchPrefillBaseActionObj(std::move(models), std::move(engine_config),
std::move(model_configs), std::move(trace_recorder)),
request_stream_callback_(std::move(request_stream_callback)) {
TVM_FFI_ICHECK(kv_state_kind_ == KVStateKind::kKVCache)
<< "Only PagedKVCache supports prefill preparation and KV migration";
}
Array<Request> Step(EngineState estate) final {
std::vector<Request> processed_requests;
// - Find the requests in `waiting_queue` that can prefill in this step.
std::optional<PrefillInput> prefill_input_opt;
while (true) {
prefill_input_opt = GetRequestStateEntriesToPrefill(estate);
if (!prefill_input_opt.has_value()) {
break;
}
PrefillInput prefill_input = prefill_input_opt.value();
int prefix_matched_length = 0;
Request request = prefill_input.rsentry->request;
processed_requests.push_back(request);
int total_input_length = 0;
for (const Data& data : request->inputs) {
total_input_length += data->GetLength();
}
{
NVTXScopedRange nvtx_scope("DisaggPrepareReceive matching prefix");
prefix_matched_length = MatchPrefixCache(estate, &prefill_input);
}
auto tstart = std::chrono::high_resolution_clock::now();
// - Update status of request states from pending to alive.
Array<String> request_ids;
std::vector<RequestState> rstates_of_entries;
std::vector<RequestStateStatus> status_before_prefill;
UpdateRequestToAlive({prefill_input}, estate, &request_ids, &rstates_of_entries,
&status_before_prefill);
// "UpdateRequestToAlive" may add the request to the engine's running request queue.
// We erase it since it's pending for the prefill instance to send the KV data over.
if (!estate->running_queue.empty() && estate->running_queue.back().same_as(request)) {
estate->running_queue.pop_back();
}
// - Add the sequence to each model.
int prefill_length = -1;
Tensor logits_for_sample{nullptr};
std::vector<Shape> kv_append_metadata;
kv_append_metadata.reserve(models_.size());
for (int model_id = 0; model_id < static_cast<int>(models_.size()); ++model_id) {
const RequestStateEntry& rsentry = prefill_input.rsentry;
RequestModelState mstate = rsentry->mstates[model_id];
Array<Data> input_data = mstate->inputs;
mstate->inputs.clear();
int input_length = prefill_input.max_prefill_length;
if (prefill_length == -1) {
prefill_length = input_length;
} else {
TVM_FFI_ICHECK_EQ(prefill_length, input_length);
}
mstate->num_prefilled_tokens += input_length;
TVM_FFI_ICHECK(mstate->draft_output_tokens.empty());
TVM_FFI_ICHECK(mstate->draft_token_slots.empty());
if (status_before_prefill[0] == RequestStateStatus::kPending &&
!estate->prefix_cache->HasSequence(mstate->internal_id)) {
// Add the sequence to the model, or fork the sequence from its parent.
// If the sequence is already in prefix cache, it has also been added/forked in the
// KVCache.
if (rsentry->parent_idx == -1) {
models_[model_id]->AddNewSequence(mstate->internal_id);
} else {
models_[model_id]->ForkSequence(
rstates_of_entries[0]->entries[rsentry->parent_idx]->mstates[model_id]->internal_id,
mstate->internal_id);
}
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
models_[model_id]->EnableSlidingWindowForSeq(mstate->internal_id);
}
}
// Record the of the prefilled inputs for prefix cache update.
for (int j = 0; j < static_cast<int>(input_data.size()); ++j) {
if (!model_id && !prefill_input.is_decode) {
mstate->prefilled_inputs.push_back(input_data[j]);
}
}
int64_t request_internal_id = mstate->internal_id;
RECORD_EVENT(trace_recorder_, request_ids, "start prefill");
Shape compressed_kv_append_metadata = {0};
if (prefill_length > 0) {
compressed_kv_append_metadata =
models_[model_id]->DisaggPrepareKVRecv(request_internal_id, prefill_length);
}
kv_append_metadata.push_back(compressed_kv_append_metadata);
RECORD_EVENT(trace_recorder_, request_ids, "finish prefill");
}
// - Commit the prefix cache changes from previous round of action.
// Note: we commit prefix cache changes here to overlap this commit with the GPU execution.
estate->prefix_cache->CommitSequenceExtention();
auto tend = std::chrono::high_resolution_clock::now();
// - Remove the request from the waiting queue.
auto it_request =
std::find(estate->waiting_queue.begin(), estate->waiting_queue.end(), request);
TVM_FFI_ICHECK(it_request != estate->waiting_queue.end());
estate->waiting_queue.erase(it_request);
{
NVTXScopedRange nvtx_scope("Call request stream callback");
tvm::ffi::json::Object response_body;
response_body.Set("prompt_length", static_cast<int64_t>(total_input_length));
response_body.Set("prefix_matched_length", static_cast<int64_t>(prefix_matched_length));
// We further flatten the metadata array of all models into a single array.
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);
}
TVM_FFI_ICHECK(!compressed_kv_append_metadata.empty());
int num_segments = compressed_kv_append_metadata[0];
TVM_FFI_ICHECK_EQ(compressed_kv_append_metadata.size(), num_segments * 2 + 1);
int transmission_length = 0;
for (int i = 0; i < num_segments; ++i) {
transmission_length += compressed_kv_append_metadata[i * 2 + 2];
}
TVM_FFI_ICHECK_EQ(transmission_length, prefill_length);
}
response_body.Set(
"kv_append_metadata",
Base64Encode(std::string(tvm::ffi::json::Stringify(kv_append_metadata_arr))));
tvm::ffi::json::Object usage;
usage.Set("prompt_tokens", static_cast<int64_t>(0));
usage.Set("completion_tokens", static_cast<int64_t>(0));
usage.Set("total_tokens", static_cast<int64_t>(0));
usage.Set("extra", response_body);
RequestStreamOutput stream_output =
RequestStreamOutput::Usage(request->id, std::string(tvm::ffi::json::Stringify(usage)));
// - Invoke the stream callback function once for all collected requests.
request_stream_callback_(Array<RequestStreamOutput>{stream_output});
}
}
for (const Request& request : processed_requests) {
TVM_FFI_ICHECK(std::find(estate->running_queue.begin(), estate->running_queue.end(),
request) == estate->running_queue.end());
}
return {processed_requests};
}
private:
// Mimicked from BatchPrefillBaseActionObj::GetRequestStateEntriesToPrefill
std::optional<PrefillInput> GetRequestStateEntriesToPrefill(EngineState estate) {
const std::vector<RequestStateEntry>* running_rsentries;
{
NVTXScopedRange nvtx_scope("BatchDecode getting requests");
running_rsentries = &estate->GetRunningRequestStateEntries();
if (!(running_rsentries->size() <= models_[0]->GetNumAvailablePages())) {
// Even the decode cannot be performed.
// As a result, directly return without doing prefill.
return {};
}
}
int num_running_rsentries = static_cast<int>(running_rsentries->size());
Request request{nullptr};
for (const Request& request_candidate : estate->waiting_queue) {
if (request_candidate->generation_cfg->debug_config.disagg_config.kind ==
DisaggRequestKind::kPrepareReceive) {
request = request_candidate;
break;
}
}
if (!request.defined()) {
// No request to prepare for prefill.
return {};
}
TVM_FFI_ICHECK_EQ(
request->generation_cfg->debug_config.disagg_config.kv_window_begin.value_or(0), 0);
std::vector<PrefillInput> prefill_input_for_all_models;
prefill_input_for_all_models.reserve(models_.size());
// We first collect the inputs that can be prefilled for each model.
// The inputs for each model are expected to be exactly the same.
for (int i = 0; i < static_cast<int>(models_.size()); ++i) {
NVTXScopedRange nvtx_scope("Process request " + request->id);
PrefillInput prefill_input;
// - Try to prefill pending requests.
int num_available_pages = models_[i]->GetNumAvailablePages();
int current_total_seq_len = models_[i]->GetCurrentTotalSequenceLength();
RequestState rstate = estate->GetRequestState(request);
bool prefill_stops = false;
for (int j = 1; j < static_cast<int>(rstate->entries.size()); ++j) {
TVM_FFI_ICHECK(rstate->entries[j]->mstates[i]->inputs.empty())
<< "Re-prefill of preempted requests is not supported by prefill preparation.";
}
const RequestStateEntry& rsentry = rstate->entries[0];
TVM_FFI_ICHECK(!rsentry->mstates[i]->inputs.empty())
<< "The request entry must have pending inputs.";
// Todo: handle the case that input length is 1.
int input_length = rsentry->mstates[i]->GetInputLength();
// Update the input length with the requested KV window, where "[begin:end]"
// means the KV range to prefill on a prefill instance.
int kv_window_begin =
request->generation_cfg->debug_config.disagg_config.kv_window_begin.value_or(0);
int kv_window_end =
request->generation_cfg->debug_config.disagg_config.kv_window_end.value_or(input_length);
TVM_FFI_ICHECK_EQ(kv_window_begin, 0);
if (kv_window_end < 0) {
kv_window_end = input_length + kv_window_end;
}
TVM_FFI_ICHECK_GE(kv_window_end, 0);
TVM_FFI_ICHECK_LT(kv_window_end, input_length)
<< "Prefill the full input on the remote machine is not supported.";
int orig_input_length = input_length;
input_length = kv_window_end;
int num_require_pages = (input_length + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
bool sliding_window_enabled = sliding_window_sizes_[i] != -1;
int num_required_pages_under_sliding_window = std::numeric_limits<int>::max();
if (sliding_window_enabled) {
// Sliding window for model i is enabled.
int max_single_request_page_requirement =
1 + (sliding_window_sizes_[i] + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
int num_total_prefilled_tokens = rsentry->mstates[i]->num_prefilled_tokens;
int num_pages_in_use = (std::min(num_total_prefilled_tokens, sliding_window_sizes_[i]) +
engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
num_required_pages_under_sliding_window =
max_single_request_page_requirement - num_pages_in_use;
num_require_pages = std::min(num_require_pages, num_required_pages_under_sliding_window);
TVM_FFI_ICHECK_GE(num_require_pages, 0);
}
// Check if the entire request state entry can fit for prefill.
bool can_prefill = false;
{
NVTXScopedRange nvtx_scope("Attempt");
for (int num_child_to_activate = rsentry->child_indices.size(); num_child_to_activate >= 0;
--num_child_to_activate) {
while (!HasPrefillSpace(num_require_pages, sliding_window_enabled, num_running_rsentries,
num_available_pages, current_total_seq_len, input_length,
engine_config_->max_total_sequence_length)) {
if (!estate->prefix_cache->TryFreeMemory()) break;
// Update number of available pages after memory free.
num_available_pages = models_[i]->GetNumAvailablePages();
current_total_seq_len = models_[i]->GetCurrentTotalSequenceLength();
}
if (CanPrefill(estate, 1 + num_child_to_activate, input_length, num_require_pages,
num_available_pages, current_total_seq_len, num_running_rsentries,
kv_state_kind_, sliding_window_enabled)) {
prefill_input = {rsentry, input_length, num_child_to_activate, /*is_decode=*/false};
can_prefill = true;
break;
}
}
}
if (!can_prefill) {
return std::nullopt;
}
rsentry->mstates[i]->inputs =
SplitData(rsentry->mstates[i]->inputs, orig_input_length, kv_window_end).first;
prefill_input_for_all_models.push_back(prefill_input);
}
// Prefill inputs of all models should be the same.
TVM_FFI_ICHECK(!prefill_input_for_all_models.empty());
PrefillInput prefill_input = prefill_input_for_all_models[0];
{
NVTXScopedRange nvtx_scope("reduction");
for (int i = 1; i < static_cast<int>(prefill_input_for_all_models.size()); ++i) {
TVM_FFI_ICHECK(prefill_input_for_all_models[i].rsentry.same_as(prefill_input.rsentry));
TVM_FFI_ICHECK_EQ(prefill_input_for_all_models[i].max_prefill_length,
prefill_input.max_prefill_length);
TVM_FFI_ICHECK_EQ(prefill_input_for_all_models[i].num_child_to_activate,
prefill_input.num_child_to_activate);
}
}
return prefill_input;
}
// Mimicked from BatchPrefillBaseActionObj::CanPrefill
bool CanPrefill(EngineState estate, int num_prefill_rsentries, int total_input_length,
int num_required_pages, int num_available_pages, int current_total_seq_len,
int num_running_rsentries, KVStateKind kv_state_kind,
bool sliding_window_enabled) {
// No exceeding of the maximum allowed requests that can
// run simultaneously.
int spec_factor = engine_config_->speculative_mode != SpeculativeMode::kDisable
? (estate->spec_draft_length + 1)
: 1;
if ((num_running_rsentries + num_prefill_rsentries) * spec_factor >
std::min(static_cast<int64_t>(engine_config_->max_num_sequence),
engine_config_->prefill_chunk_size)) {
return false;
}
// NOTE: The conditions are heuristic and can be revised.
// Cond 1: at least one decode can be performed after prefill.
// Cond 2: number of total tokens after "x" times of decode does not
// exceed the limit, where "x" is a watermark number can
// be configured and adjusted in the future.
if (num_required_pages + 400 > num_available_pages) {
return false;
}
return HasPrefillSpace(num_required_pages, sliding_window_enabled,
(num_running_rsentries + num_prefill_rsentries), num_available_pages,
current_total_seq_len, total_input_length,
engine_config_->max_total_sequence_length);
}
// Mimicked from NewRequestPrefillActionObj::MatchPrefixCache
int MatchPrefixCache(EngineState estate, PrefillInput* input) final {
RequestStateEntry rsentry = input->rsentry;
if (estate->prefix_cache->Mode() == PrefixCacheMode::kDisable) {
return 0;
}
if (rsentry->parent_idx == -1 && rsentry->status == RequestStateStatus::kPending &&
!estate->prefix_cache->HasSequence(rsentry->mstates[0]->internal_id)) {
std::vector<int32_t> tokens = GetConcatPrefillInputData(rsentry->mstates[0]);
if (tokens.empty()) {
// If the RequestStateEntry is of empty input data, or not fully tokenized, do nothing
// and return.
return 0;
}
PrefixCacheMatchedResult result = estate->prefix_cache->InsertSequence(
rsentry->mstates[0]->internal_id, tokens, models_[0]->GetSlidingWindowSize(),
models_[0]->GetAttentionSinkSize());
if (result.prefilled_offset == 0) {
// Add new sequence
TVM_FFI_ICHECK_EQ(result.forked_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_pop_last_tokens, 0);
for (Model model : models_) {
model->AddNewSequence(rsentry->mstates[0]->internal_id);
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
model->EnableSlidingWindowForSeq(rsentry->mstates[0]->internal_id);
}
}
} else {
if (result.forked_seq_id != -1) {
TVM_FFI_ICHECK_EQ(result.reused_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_pop_last_tokens, 0);
// Fork from active sequence
for (Model model : models_) {
model->ForkSequence(result.forked_seq_id, rsentry->mstates[0]->internal_id,
result.prefilled_offset);
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
model->EnableSlidingWindowForSeq(rsentry->mstates[0]->internal_id);
}
}
} else {
// Reuse recycling sequence
TVM_FFI_ICHECK_EQ(result.forked_seq_id, -1);
estate->id_manager.RecycleId(rsentry->mstates[0]->internal_id);
for (int i = 0; i < rsentry->mstates.size(); ++i) {
rsentry->mstates[i]->internal_id = result.reused_seq_id;
}
if (result.reused_seq_pop_last_tokens > 0) {
for (Model model : models_) {
model->PopNFromKVCache(rsentry->mstates[0]->internal_id,
result.reused_seq_pop_last_tokens);
}
}
}
}
// Pop matched prefix
if (result.prefilled_offset) {
for (int i = 0; i < rsentry->mstates.size(); ++i) {
PopPrefillInputData(rsentry->mstates[i], result.prefilled_offset);
}
}
// Update max prefill length
input->max_prefill_length =
std::min(input->max_prefill_length, rsentry->mstates[0]->GetInputLength());
return result.prefilled_offset;
}
return 0;
}
/*!
* \brief The stream callback function to passes back the KV cache metadata
* and prefix matched length in prefix cache.
*/
FRequestStreamCallback request_stream_callback_;
};
EngineAction EngineAction::DisaggPrepareReceive(Array<Model> models, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder,
FRequestStreamCallback request_stream_callback) {
return EngineAction(tvm::ffi::make_object<DisaggPrepareReceiveActionObj>(
std::move(models), std::move(engine_config), std::move(model_configs),
std::move(trace_recorder), std::move(request_stream_callback)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
@@ -0,0 +1,503 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/new_request_prefill.cc
*/
#include <tvm/support/cuda/nvtx.h>
#include "../sampler/sampler.h"
#include "batch_prefill_base.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
/*!
* \brief The action that prefills requests in the `waiting_queue` of
* the engine state.
* Aside from that, this action sends the computed KV data to remote
* instances after computing the KV data.
*/
class DisaggRemoteSendActionObj : public BatchPrefillBaseActionObj {
public:
explicit DisaggRemoteSendActionObj(Array<Model> models,
std::vector<ModelWorkspace> model_workspaces,
EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder,
FRequestStreamCallback request_stream_callback, Device device)
: BatchPrefillBaseActionObj(std::move(models), std::move(engine_config),
std::move(model_configs), std::move(trace_recorder)),
model_workspaces_(std::move(model_workspaces)),
request_stream_callback_(std::move(request_stream_callback)),
device_(device) {
if (device.device_type == DLDeviceType::kDLCUDA ||
device.device_type == DLDeviceType::kDLROCM) {
// The compute stream is the default stream.
compute_stream_ = DeviceAPI::Get(device)->GetCurrentStream(device);
}
}
// Mimicked from NewRequestPrefillActionObj::Step
Array<Request> Step(EngineState estate) final {
// - Find the requests in `waiting_queue` that can prefill in this step.
std::vector<PrefillInput> prefill_inputs;
{
NVTXScopedRange nvtx_scope("DisaggRemoteSend getting requests");
prefill_inputs = GetRequestStateEntriesToPrefill(estate);
if (prefill_inputs.empty()) {
return {};
}
}
int num_rsentries = prefill_inputs.size();
{
NVTXScopedRange nvtx_scope("DisaggRemoteSend matching prefix");
for (int i = 0; i < num_rsentries; ++i) {
MatchPrefixCache(estate, &prefill_inputs[i]);
}
}
auto tstart = std::chrono::high_resolution_clock::now();
// - Update status of request states from pending to alive.
Array<String> request_ids;
std::vector<RequestState> rstates_of_entries;
std::vector<RequestStateStatus> status_before_prefill;
UpdateRequestToAlive(prefill_inputs, estate, &request_ids, &rstates_of_entries,
&status_before_prefill);
// - Get embedding and run prefill for each model.
// NOTE: we don't keep the logits as we don't run sampling in this action by design.
std::vector<int> prefill_lengths;
prefill_lengths.resize(/*size=*/num_rsentries, /*value=*/-1);
for (int model_id = 0; model_id < static_cast<int>(models_.size()); ++model_id) {
std::vector<int64_t> request_internal_ids;
request_internal_ids.reserve(num_rsentries);
ObjectRef embeddings = model_workspaces_[model_id].embeddings;
int cum_prefill_length = 0;
bool single_input =
num_rsentries == 1 && prefill_inputs[0].rsentry->mstates[model_id]->inputs.size() == 1;
std::vector<int64_t> cached_token_data;
for (int i = 0; i < num_rsentries; ++i) {
const RequestStateEntry& rsentry = prefill_inputs[i].rsentry;
RequestModelState mstate = rsentry->mstates[model_id];
auto [input_data, input_length] =
ChunkPrefillInputData(mstate, prefill_inputs[i].max_prefill_length);
if (prefill_lengths[i] == -1) {
prefill_lengths[i] = input_length;
} else {
TVM_FFI_ICHECK_EQ(prefill_lengths[i], input_length);
}
mstate->num_prefilled_tokens += input_length;
TVM_FFI_ICHECK(mstate->draft_output_tokens.empty());
TVM_FFI_ICHECK(mstate->draft_token_slots.empty());
if (status_before_prefill[i] == RequestStateStatus::kPending &&
!estate->prefix_cache->HasSequence(mstate->internal_id)) {
// Add the sequence to the model.
// If the sequence is already in prefix cache, it has also been added/forked in the
// KVCache.
TVM_FFI_ICHECK_EQ(rsentry->parent_idx, -1);
models_[model_id]->AddNewSequence(mstate->internal_id);
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
models_[model_id]->EnableSlidingWindowForSeq(mstate->internal_id);
}
DisaggConfig disagg_config = mstate->request->generation_cfg->debug_config.disagg_config;
TVM_FFI_ICHECK(disagg_config.dst_group_offset.has_value());
models_[model_id]->DisaggMarkKVSend(
mstate->internal_id, disagg_config.kv_window_begin.value_or(0),
disagg_config.kv_append_metadata[model_id], disagg_config.dst_group_offset.value());
}
request_internal_ids.push_back(mstate->internal_id);
RECORD_EVENT(trace_recorder_, rsentry->request->id, "start embedding");
for (int j = 0; j < static_cast<int>(input_data.size()); ++j) {
if (!model_id && !prefill_inputs[i].is_decode) {
mstate->prefilled_inputs.push_back(input_data[j]);
}
if (const auto* token_data = input_data[j].as<TokenDataNode>()) {
cached_token_data.insert(cached_token_data.end(), token_data->token_ids.begin(),
token_data->token_ids.end());
} else {
if (!cached_token_data.empty()) {
embeddings = TokenData(cached_token_data)
->GetEmbedding(models_[model_id],
/*dst=*/!single_input ? &embeddings : nullptr,
/*offset=*/cum_prefill_length);
cum_prefill_length += cached_token_data.size();
cached_token_data.clear();
}
embeddings = input_data[j]->GetEmbedding(models_[model_id],
/*dst=*/!single_input ? &embeddings : nullptr,
/*offset=*/cum_prefill_length);
cum_prefill_length += input_data[j]->GetLength();
}
}
RECORD_EVENT(trace_recorder_, rsentry->request->id, "finish embedding");
}
if (!cached_token_data.empty()) {
embeddings = TokenData(cached_token_data)
->GetEmbedding(models_[model_id],
/*dst=*/!single_input ? &embeddings : nullptr,
/*offset=*/cum_prefill_length);
cum_prefill_length += cached_token_data.size();
cached_token_data.clear();
}
RECORD_EVENT(trace_recorder_, request_ids, "start prefill");
Tensor logits =
models_[model_id]->BatchPrefill(embeddings, request_internal_ids, prefill_lengths);
RECORD_EVENT(trace_recorder_, request_ids, "finish prefill");
TVM_FFI_ICHECK_EQ(logits->ndim, 3);
TVM_FFI_ICHECK_EQ(logits->shape[0], 1);
TVM_FFI_ICHECK_EQ(logits->shape[1], num_rsentries);
}
// - Commit the prefix cache changes from previous round of action.
// Note: we commit prefix cache changes here to overlap this commit with the GPU execution.
estate->prefix_cache->CommitSequenceExtention();
// - We run synchronize to make sure that the prefill is finished.
// We need explicit synchronization because we don't do sampling in this action.
DeviceAPI::Get(device_)->StreamSync(device_, compute_stream_);
auto tend = std::chrono::high_resolution_clock::now();
estate->metrics.engine_prefill_time_sum += static_cast<double>((tend - tstart).count()) / 1e9;
std::vector<Request> processed_requests =
RemoveProcessedRequests(prefill_inputs, estate, rstates_of_entries);
estate->running_rsentries_changed = true;
return processed_requests;
}
private:
// Mimicked from BatchPrefillBaseActionObj::GetRequestStateEntriesToPrefill
std::vector<PrefillInput> GetRequestStateEntriesToPrefill(EngineState estate) {
// Preempt request state entries when decode cannot apply.
const std::vector<RequestStateEntry>* running_rsentries;
{
NVTXScopedRange nvtx_scope("BatchDecode getting requests");
running_rsentries = &estate->GetRunningRequestStateEntries();
if (!(running_rsentries->size() <= models_[0]->GetNumAvailablePages())) {
// Even the decode cannot be performed.
// As a result, directly return without doing prefill.
return {};
}
}
// Explicitly filter the waiting queue to only keep the requests
// with disaggregation request kind "kRemoteSend".
std::vector<Request> waiting_queue;
waiting_queue.reserve(estate->waiting_queue.size());
for (Request request : estate->waiting_queue) {
if (request->generation_cfg->debug_config.disagg_config.kind ==
DisaggRequestKind::kRemoteSend) {
waiting_queue.push_back(request);
}
}
if (waiting_queue.empty()) {
// No request to prefill.
return {};
}
std::vector<std::vector<PrefillInput>> prefill_inputs_for_all_models;
prefill_inputs_for_all_models.reserve(models_.size());
int num_running_rsentries = static_cast<int>(running_rsentries->size());
// We first collect the inputs that can be prefilled for each model.
// Then we make a reduction to return the maximum common inputs.
for (int i = 0; i < static_cast<int>(models_.size()); ++i) {
std::vector<PrefillInput> prefill_inputs;
// - Try to prefill pending requests.
int total_input_length = 0;
int total_required_pages = 0;
int num_available_pages;
int current_total_seq_len;
{
NVTXScopedRange nvtx_scope("KV cache GetNumAvailablePages");
num_available_pages = models_[i]->GetNumAvailablePages();
}
{
NVTXScopedRange nvtx_scope("KV cache GetCurrentTotalSequenceLength");
current_total_seq_len = models_[i]->GetCurrentTotalSequenceLength();
}
int num_prefill_rsentries = 0;
for (const Request& request : waiting_queue) {
NVTXScopedRange nvtx_scope("Process request " + request->id);
RequestState rstate = estate->GetRequestState(request);
TVM_FFI_ICHECK_EQ(rstate->entries.size(), 1) << "n > 1 is not supported.";
const RequestStateEntry& rsentry = rstate->entries[0];
TVM_FFI_ICHECK(!rsentry->mstates[i]->inputs.empty())
<< "The request entry must have pending inputs.";
int input_length = rsentry->mstates[i]->GetInputLength();
int num_require_pages = (input_length + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
bool sliding_window_enabled = sliding_window_sizes_[i] != -1;
int num_required_pages_under_sliding_window = std::numeric_limits<int>::max();
if (sliding_window_enabled) {
// Sliding window for model i is enabled.
int max_single_request_page_requirement =
1 + (sliding_window_sizes_[i] + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
int num_total_prefilled_tokens = rsentry->mstates[i]->num_prefilled_tokens;
int parent_ptr = rsentry->parent_idx;
while (parent_ptr != -1) {
num_total_prefilled_tokens +=
rstate->entries[parent_ptr]->mstates[i]->num_prefilled_tokens;
parent_ptr = rstate->entries[parent_ptr]->parent_idx;
}
int num_pages_in_use = (std::min(num_total_prefilled_tokens, sliding_window_sizes_[i]) +
engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
num_required_pages_under_sliding_window =
max_single_request_page_requirement - num_pages_in_use;
num_require_pages = std::min(num_require_pages, num_required_pages_under_sliding_window);
TVM_FFI_ICHECK_GE(num_require_pages, 0);
}
total_input_length += input_length;
total_required_pages += num_require_pages;
// - Attempt 1. Check if the entire request state entry can fit for prefill.
bool can_prefill = false;
{
NVTXScopedRange nvtx_scope("Attempt 1");
for (int num_child_to_activate = rsentry->child_indices.size();
num_child_to_activate >= 0; --num_child_to_activate) {
while (!HasPrefillSpace(total_required_pages, sliding_window_enabled,
(num_running_rsentries + num_prefill_rsentries),
num_available_pages, current_total_seq_len, total_input_length,
engine_config_->max_total_sequence_length)) {
if (!estate->prefix_cache->TryFreeMemory()) break;
// Update number of available pages after memory free.
num_available_pages = models_[i]->GetNumAvailablePages();
current_total_seq_len = models_[i]->GetCurrentTotalSequenceLength();
}
if (CanPrefill(estate, num_prefill_rsentries + 1 + num_child_to_activate,
total_input_length, total_required_pages, num_available_pages,
current_total_seq_len, num_running_rsentries, kv_state_kind_,
sliding_window_enabled)) {
prefill_inputs.push_back(
{rsentry, input_length, num_child_to_activate, /*is_decode=*/false});
num_prefill_rsentries += 1 + num_child_to_activate;
can_prefill = true;
break;
}
}
}
if (can_prefill) {
continue;
}
total_input_length -= input_length;
total_required_pages -= num_require_pages;
// - Attempt 2. Check if the request state entry can partially fit by input chunking.
TVM_FFI_ICHECK_LE(total_input_length, engine_config_->prefill_chunk_size);
if (engine_config_->prefill_chunk_size - total_input_length >= input_length ||
engine_config_->prefill_chunk_size == total_input_length) {
// 1. If the input length can fit the remaining prefill chunk size,
// it means the failure of attempt 1 is not because of the input
// length being too long, and thus chunking does not help.
// 2. If the total input length already reaches the prefill chunk size,
// the current request state entry will not be able to be processed.
// So we can safely return in either case.
break;
}
input_length = engine_config_->prefill_chunk_size - total_input_length;
num_require_pages = (input_length + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
if (sliding_window_enabled) {
// Sliding window for model i is enabled.
num_require_pages = std::min(num_require_pages, num_required_pages_under_sliding_window);
TVM_FFI_ICHECK_GE(num_require_pages, 0);
}
{
NVTXScopedRange nvtx_scope("Attempt 2");
total_input_length += input_length;
total_required_pages += num_require_pages;
if (CanPrefill(estate, num_prefill_rsentries + 1, total_input_length,
total_required_pages, num_available_pages, current_total_seq_len,
num_running_rsentries, kv_state_kind_, sliding_window_enabled)) {
prefill_inputs.push_back({rsentry, input_length, 0, /*is_decode=*/false});
}
}
// - Prefill stops here.
break;
}
prefill_inputs_for_all_models.push_back(prefill_inputs);
}
// Reduce over the prefill inputs of all models.
TVM_FFI_ICHECK(!prefill_inputs_for_all_models.empty());
int num_prefill_inputs = prefill_inputs_for_all_models[0].size();
for (int i = 1; i < static_cast<int>(prefill_inputs_for_all_models.size()); ++i) {
num_prefill_inputs =
std::min(num_prefill_inputs, static_cast<int>(prefill_inputs_for_all_models[i].size()));
}
if (num_prefill_inputs == 0) {
return {};
}
// Add the decode requests to the prefill inputs if prefill mode is hybrid.
std::vector<PrefillInput> prefill_inputs(prefill_inputs_for_all_models[0].begin(),
prefill_inputs_for_all_models[0].end());
{
NVTXScopedRange nvtx_scope("reduction");
for (int i = 1; i < static_cast<int>(prefill_inputs_for_all_models.size()); ++i) {
// Prefill input lengths except the last one are supposed to be the same for all models.
for (int j = 0; j < num_prefill_inputs - 1; ++j) {
TVM_FFI_ICHECK(
prefill_inputs_for_all_models[i][j].rsentry.same_as(prefill_inputs[j].rsentry));
TVM_FFI_ICHECK_EQ(prefill_inputs_for_all_models[i][j].max_prefill_length,
prefill_inputs[j].max_prefill_length);
prefill_inputs[j].num_child_to_activate =
std::min(prefill_inputs[j].num_child_to_activate,
prefill_inputs_for_all_models[i][j].num_child_to_activate);
}
// The input length of the last input is the minimum among all models.
TVM_FFI_ICHECK(prefill_inputs_for_all_models[i][num_prefill_inputs - 1].rsentry.same_as(
prefill_inputs[num_prefill_inputs - 1].rsentry));
prefill_inputs[num_prefill_inputs - 1].max_prefill_length =
std::min(prefill_inputs[num_prefill_inputs - 1].max_prefill_length,
prefill_inputs_for_all_models[i][num_prefill_inputs - 1].max_prefill_length);
prefill_inputs[num_prefill_inputs - 1].num_child_to_activate = std::min(
prefill_inputs[num_prefill_inputs - 1].num_child_to_activate,
prefill_inputs_for_all_models[i][num_prefill_inputs - 1].num_child_to_activate);
}
}
return prefill_inputs;
}
// Copied from NewRequestPrefillActionObj::MatchPrefixCache
/*!
* \brief Match the request state entry with prefix cache, to skip prefilling common prefix
* tokens. If the request state entry is not added to KVCache yet, this method will add/fork the
* request in the KVCache, depending on the matching result from prefix cache.
* \param estate The engine state.
* \param[in, out] input The prefill input to be matched and updated.
* \return The matched length in prefix cache.
*/
int MatchPrefixCache(EngineState estate, PrefillInput* input) final {
RequestStateEntry rsentry = input->rsentry;
if (estate->prefix_cache->Mode() == PrefixCacheMode::kDisable) {
return 0;
}
if (rsentry->parent_idx == -1 && rsentry->status == RequestStateStatus::kPending &&
!estate->prefix_cache->HasSequence(rsentry->mstates[0]->internal_id)) {
std::vector<int32_t> tokens = GetConcatPrefillInputData(rsentry->mstates[0]);
if (tokens.empty()) {
// If the RequestStateEntry is of empty input data, or not fully tokenized, do nothing
// and return.
return 0;
}
PrefixCacheMatchedResult result = estate->prefix_cache->InsertSequence(
rsentry->mstates[0]->internal_id, tokens, models_[0]->GetSlidingWindowSize(),
models_[0]->GetAttentionSinkSize());
if (result.prefilled_offset == 0) {
// Add new sequence
TVM_FFI_ICHECK_EQ(result.forked_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_pop_last_tokens, 0);
for (int model_id = 0; model_id < static_cast<int>(models_.size()); ++model_id) {
Model model = models_[model_id];
RequestModelState mstate = rsentry->mstates[model_id];
model->AddNewSequence(rsentry->mstates[0]->internal_id);
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
model->EnableSlidingWindowForSeq(rsentry->mstates[0]->internal_id);
}
DisaggConfig disagg_config = mstate->request->generation_cfg->debug_config.disagg_config;
models_[model_id]->DisaggMarkKVSend(
mstate->internal_id, disagg_config.kv_window_begin.value_or(0),
disagg_config.kv_append_metadata[model_id], disagg_config.dst_group_offset.value());
}
} else {
if (result.forked_seq_id != -1) {
TVM_FFI_ICHECK_EQ(result.reused_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_pop_last_tokens, 0);
// Fork from active sequence
for (int model_id = 0; model_id < static_cast<int>(models_.size()); ++model_id) {
Model model = models_[model_id];
RequestModelState mstate = rsentry->mstates[model_id];
model->ForkSequence(result.forked_seq_id, rsentry->mstates[0]->internal_id,
result.prefilled_offset);
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
model->EnableSlidingWindowForSeq(rsentry->mstates[0]->internal_id);
}
DisaggConfig disagg_config =
mstate->request->generation_cfg->debug_config.disagg_config;
models_[model_id]->DisaggMarkKVSend(
mstate->internal_id, disagg_config.kv_window_begin.value_or(0),
disagg_config.kv_append_metadata[model_id], disagg_config.dst_group_offset.value());
}
} else {
// Reuse recycling sequence
TVM_FFI_ICHECK_EQ(result.forked_seq_id, -1);
estate->id_manager.RecycleId(rsentry->mstates[0]->internal_id);
for (int i = 0; i < rsentry->mstates.size(); ++i) {
rsentry->mstates[i]->internal_id = result.reused_seq_id;
}
if (result.reused_seq_pop_last_tokens > 0) {
for (Model model : models_) {
model->PopNFromKVCache(rsentry->mstates[0]->internal_id,
result.reused_seq_pop_last_tokens);
}
}
for (int model_id = 0; model_id < static_cast<int>(models_.size()); ++model_id) {
RequestModelState mstate = rsentry->mstates[model_id];
DisaggConfig disagg_config =
mstate->request->generation_cfg->debug_config.disagg_config;
models_[model_id]->DisaggMarkKVSend(
mstate->internal_id, disagg_config.kv_window_begin.value_or(0),
disagg_config.kv_append_metadata[model_id], disagg_config.dst_group_offset.value());
}
}
}
// Pop matched prefix
if (result.prefilled_offset) {
for (int i = 0; i < rsentry->mstates.size(); ++i) {
PopPrefillInputData(rsentry->mstates[i], result.prefilled_offset);
}
}
// Update max prefill length
input->max_prefill_length =
std::min(input->max_prefill_length, rsentry->mstates[0]->GetInputLength());
return result.prefilled_offset;
}
return 0;
}
/*! \brief Workspace of each model. */
std::vector<ModelWorkspace> model_workspaces_;
/*! \brief The stream callback function to passes back the sampled results after prefill. */
FRequestStreamCallback request_stream_callback_;
/*! \brief The device which we run synchronization for after prefill. */
Device device_;
/*! \brief The compute stream to run synchronization for. */
TVMStreamHandle compute_stream_ = nullptr;
};
EngineAction EngineAction::DisaggRemoteSend(
Array<Model> models, std::vector<ModelWorkspace> model_workspaces, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs, Optional<EventTraceRecorder> trace_recorder,
FRequestStreamCallback request_stream_callback, Device device) {
return EngineAction(tvm::ffi::make_object<DisaggRemoteSendActionObj>(
std::move(models), std::move(model_workspaces), std::move(engine_config),
std::move(model_configs), std::move(trace_recorder), std::move(request_stream_callback),
device));
}
} // namespace serve
} // namespace llm
} // namespace mlc
@@ -0,0 +1,234 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/eagle_batch_draft.cc
*/
#include <numeric>
#include "../config.h"
#include "../model.h"
#include "../sampler/sampler.h"
#include "action.h"
#include "action_commons.h"
namespace mlc {
namespace llm {
namespace serve {
/*!
* \brief The action that runs draft proposal for requests in the
* `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
*/
class EagleBatchDraftActionObj : public EngineActionObj {
public:
explicit EagleBatchDraftActionObj(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler, std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder)
: models_(std::move(models)),
logit_processor_(std::move(logit_processor)),
sampler_(std::move(sampler)),
model_workspaces_(std::move(model_workspaces)),
draft_token_workspace_manager_(std::move(draft_token_workspace_manager)),
engine_config_(std::move(engine_config)),
trace_recorder_(std::move(trace_recorder)) {}
Array<Request> Step(EngineState estate) final {
// - Only run spec decode when there are two models (llm+ssm) and >=1 running requests.
if (models_.size() != 2 || estate->running_queue.empty()) {
return {};
}
// Preempt request state entries when decode cannot apply.
std::vector<RequestStateEntry> running_rsentries = estate->GetRunningRequestStateEntries();
while (!CanDecode(running_rsentries.size())) {
if (estate->prefix_cache->TryFreeMemory()) continue;
RequestStateEntry preempted = PreemptLastRunningRequestStateEntry(
estate, models_, draft_token_workspace_manager_, trace_recorder_);
if (preempted.same_as(running_rsentries.back())) {
running_rsentries.pop_back();
}
}
auto tstart = std::chrono::high_resolution_clock::now();
int num_rsentries = running_rsentries.size();
TVM_FFI_ICHECK_GT(num_rsentries, 0)
<< "There should be at least one request state entry that can run decode. "
"Possible failure reason: none of the prefill phase of the running requests is finished";
TVM_FFI_ICHECK_LE(num_rsentries, engine_config_->max_num_sequence)
<< "The number of running requests exceeds the max number of sequence in EngineConfig. "
"Possible failure reason: the prefill action allows new sequence in regardless of the "
"max num sequence.";
Array<String> request_ids;
std::vector<int64_t> request_internal_ids;
Array<GenerationConfig> generation_cfg;
std::vector<RandomGenerator*> rngs;
std::vector<std::vector<int>> draft_token_indices;
request_ids.reserve(num_rsentries);
request_internal_ids.reserve(num_rsentries);
generation_cfg.reserve(num_rsentries);
draft_token_indices.reserve(num_rsentries);
for (const RequestStateEntry& rsentry : running_rsentries) {
request_ids.push_back(rsentry->request->id);
request_internal_ids.push_back(rsentry->mstates[0]->internal_id);
generation_cfg.push_back(rsentry->request->generation_cfg);
rngs.push_back(&rsentry->rng);
}
TVM_FFI_ICHECK_GT(estate->spec_draft_length, 0)
<< "The speculative decoding draft length must be positive.";
// The first model doesn't get involved in draft proposal.
for (int model_id = 1; model_id < static_cast<int>(models_.size()); ++model_id) {
// Collect
// - the last committed token,
// - the request model state
// of each request.
std::vector<int> input_tokens;
Array<RequestModelState> mstates;
input_tokens.reserve(num_rsentries);
mstates.reserve(num_rsentries);
for (const RequestStateEntry& rsentry : running_rsentries) {
mstates.push_back(rsentry->mstates[model_id]);
}
// draft_length_ rounds of draft proposal.
ObjectRef hidden_states = model_workspaces_[model_id].hidden_states;
// Concat last hidden_states
draft_token_slots_.clear();
if (estate->spec_draft_length > 1) {
for (int i = 0; i < num_rsentries; ++i) {
draft_token_slots_.push_back(mstates[i]->draft_token_slots.back());
}
hidden_states = models_[model_id]->GatherHiddenStates(
model_workspaces_[0].draft_hidden_states_storage, draft_token_slots_, &hidden_states);
}
// The first draft token has been generated in prefill/verify stage
for (int draft_id = 1; draft_id < estate->spec_draft_length; ++draft_id) {
draft_token_indices.clear();
auto tdraft_start = std::chrono::high_resolution_clock::now();
// prepare new input tokens
input_tokens.clear();
for (int i = 0; i < num_rsentries; ++i) {
TVM_FFI_ICHECK(!mstates[i]->draft_output_tokens.empty());
input_tokens.push_back(mstates[i]->draft_output_tokens.back().GetTokenId());
draft_token_indices.emplace_back(
std::vector<int>{static_cast<int>(mstates[i]->draft_output_tokens.size() - 1)});
}
// - Compute embeddings.
RECORD_EVENT(trace_recorder_, request_ids, "start proposal embedding");
ObjectRef embeddings =
models_[model_id]->TokenEmbed({Shape{input_tokens.begin(), input_tokens.end()}});
RECORD_EVENT(trace_recorder_, request_ids, "finish proposal embedding");
// - Invoke model decode.
RECORD_EVENT(trace_recorder_, request_ids, "start proposal decode");
ObjectRef fused_embedding_hidden_states = models_[model_id]->FuseEmbedHidden(
embeddings, hidden_states, /*batch_size*/ num_rsentries, /*seq_len*/ 1);
hidden_states = models_[model_id]->BatchDecodeToLastHidden(fused_embedding_hidden_states,
request_internal_ids);
Tensor logits;
if (models_[model_id]->CanGetLogits()) {
logits = models_[model_id]->GetLogits(hidden_states);
} else {
// - Use base model's head.
logits = models_[0]->GetLogits(hidden_states);
}
RECORD_EVENT(trace_recorder_, request_ids, "finish proposal decode");
TVM_FFI_ICHECK_EQ(logits->ndim, 2);
TVM_FFI_ICHECK_EQ(logits->shape[0], num_rsentries);
// - Update logits.
logit_processor_->InplaceUpdateLogits(logits, generation_cfg, mstates, request_ids, nullptr,
&mstates, &draft_token_indices);
// - Compute probability distributions.
Tensor probs_on_device =
logit_processor_->ComputeProbsFromLogits(logits, generation_cfg, request_ids);
// - Commit the prefix cache changes from previous round of action.
// Note: we commit prefix cache changes here to overlap this commit with the GPU execution.
estate->prefix_cache->CommitSequenceExtention();
// - Sample tokens.
// Fill range [0, num_rsentries) into `sample_indices`.
std::vector<int> sample_indices(num_rsentries);
std::iota(sample_indices.begin(), sample_indices.end(), 0);
Tensor renormalized_probs = sampler_->BatchRenormalizeProbsByTopP(
probs_on_device, sample_indices, request_ids, generation_cfg);
std::vector<SampleResult> sample_results = sampler_->BatchSampleTokensWithProbAfterTopP(
renormalized_probs, sample_indices, request_ids, generation_cfg, rngs);
TVM_FFI_ICHECK_EQ(sample_results.size(), num_rsentries);
// - Add draft token to the state.
draft_token_workspace_manager_->AllocSlots(num_rsentries, &draft_token_slots_);
models_[model_id]->ScatterDraftProbs(probs_on_device, draft_token_slots_,
&model_workspaces_[0].draft_probs_storage);
// No need to save hidden states as they are not used by subsequent engine actions
for (int i = 0; i < num_rsentries; ++i) {
int64_t parent_idx = static_cast<int64_t>(mstates[i]->draft_output_tokens.size()) - 1;
mstates[i]->AddDraftToken(sample_results[i], draft_token_slots_[i], parent_idx);
}
auto tdraft_end = std::chrono::high_resolution_clock::now();
estate->metrics.UpdateDraftTimeByBatchSize(
num_rsentries, static_cast<double>((tdraft_end - tdraft_start).count()) / 1e9);
}
}
auto tend = std::chrono::high_resolution_clock::now();
estate->metrics.engine_decode_time_sum += static_cast<double>((tend - tstart).count()) / 1e9;
return {};
}
private:
/*! \brief Check if the input requests can be decoded under conditions. */
bool CanDecode(int num_rsentries) {
// The first model is not involved in draft proposal.
for (int model_id = 1; model_id < static_cast<int>(models_.size()); ++model_id) {
// Check if the model has enough available pages.
int num_available_pages = models_[model_id]->GetNumAvailablePages();
if (num_rsentries > num_available_pages) {
return false;
}
}
return true;
}
/*! \brief The model to run draft generation in speculative decoding. */
Array<Model> models_;
/*! \brief The logit processor. */
LogitProcessor logit_processor_;
/*! \brief The sampler to sample new tokens. */
Sampler sampler_;
/*! \brief Workspace of each model. */
std::vector<ModelWorkspace> model_workspaces_;
/*! \brief The draft token workspace manager. */
DraftTokenWorkspaceManager draft_token_workspace_manager_;
/*! \brief The engine config. */
EngineConfig engine_config_;
/*! \brief Event trace recorder. */
Optional<EventTraceRecorder> trace_recorder_;
/*! \brief Temporary buffer to store the slots of the current draft tokens */
std::vector<int> draft_token_slots_;
};
EngineAction EngineAction::EagleBatchDraft(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder) {
return EngineAction(tvm::ffi::make_object<EagleBatchDraftActionObj>(
std::move(models), std::move(logit_processor), std::move(sampler),
std::move(model_workspaces), std::move(draft_token_workspace_manager),
std::move(engine_config), std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
@@ -0,0 +1,460 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/eagle_batch_verify.cc
*/
#include <cmath>
#include <exception>
#include <numeric>
#include "../../support/random.h"
#include "../config.h"
#include "../model.h"
#include "../sampler/sampler.h"
#include "action.h"
#include "action_commons.h"
namespace mlc {
namespace llm {
namespace serve {
/*!
* \brief The action that runs verification for requests in the
* `running_queue` of engine state. Preempt low-priority requests
* accordingly when it is impossible to decode all the running requests.
*/
class EagleBatchVerifyActionObj : public EngineActionObj {
public:
explicit EagleBatchVerifyActionObj(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler, std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder)
: models_(std::move(models)),
logit_processor_(std::move(logit_processor)),
sampler_(std::move(sampler)),
model_workspaces_(std::move(model_workspaces)),
draft_token_workspace_manager_(std::move(draft_token_workspace_manager)),
engine_config_(std::move(engine_config)),
trace_recorder_(std::move(trace_recorder)),
rng_(RandomGenerator::GetInstance()) {}
Array<Request> Step(EngineState estate) final {
// - Only run spec decode when there are two models (llm+ssm) and >=1 running requests.
if (models_.size() != 2 || estate->running_queue.empty()) {
return {};
}
const auto& [rsentries, draft_lengths, total_draft_length] = GetDraftsToVerify(estate);
TVM_FFI_ICHECK_EQ(rsentries.size(), draft_lengths.size());
if (rsentries.empty()) {
return {};
}
auto tstart = std::chrono::high_resolution_clock::now();
int num_rsentries = rsentries.size();
Array<String> request_ids =
rsentries.Map([](const RequestStateEntry& rstate) { return rstate->request->id; });
// - Get embedding and run verify.
std::vector<int64_t> request_internal_ids;
std::vector<int32_t> all_tokens_to_verify;
Array<RequestModelState> verify_request_mstates;
Array<RequestModelState> draft_request_mstates;
Array<GenerationConfig> generation_cfg;
std::vector<RandomGenerator*> rngs;
std::vector<std::vector<SampleResult>> draft_output_tokens;
std::vector<std::vector<int>> draft_token_indices;
std::vector<int64_t> token_tree_parent_ptr;
request_internal_ids.reserve(num_rsentries);
all_tokens_to_verify.reserve(total_draft_length);
token_tree_parent_ptr.reserve(total_draft_length);
verify_request_mstates.reserve(num_rsentries);
draft_request_mstates.reserve(num_rsentries);
rngs.reserve(num_rsentries);
generation_cfg.reserve(num_rsentries);
draft_output_tokens.reserve(num_rsentries);
draft_token_indices.reserve(num_rsentries);
draft_token_slots_.clear();
for (int i = 0; i < num_rsentries; ++i) {
RequestModelState verify_mstate = rsentries[i]->mstates[verify_model_id_];
RequestModelState draft_mstate = rsentries[i]->mstates[draft_model_id_];
request_internal_ids.push_back(verify_mstate->internal_id);
TVM_FFI_ICHECK(!draft_lengths.empty());
TVM_FFI_ICHECK_EQ(draft_lengths[i], draft_mstate->draft_output_tokens.size());
TVM_FFI_ICHECK_EQ(draft_lengths[i], draft_mstate->draft_token_slots.size());
// the last committed token + all the draft tokens but the last one.
all_tokens_to_verify.push_back(draft_mstate->committed_tokens.back().GetTokenId());
draft_token_slots_.push_back(0); // placeholder for the last committed token
token_tree_parent_ptr.push_back(-1);
for (int j = 0; j < static_cast<int>(draft_mstate->draft_output_tokens.size()); ++j) {
all_tokens_to_verify.push_back(draft_mstate->draft_output_tokens[j].GetTokenId());
draft_token_slots_.push_back(draft_mstate->draft_token_slots[j]);
token_tree_parent_ptr.push_back(draft_mstate->draft_token_parent_idx[j] + 1);
}
std::vector<int> cur_draft_token_indices(draft_mstate->draft_output_tokens.size() + 1);
std::iota(cur_draft_token_indices.begin(), cur_draft_token_indices.end(), -1);
draft_token_indices.emplace_back(std::move(cur_draft_token_indices));
verify_request_mstates.push_back(verify_mstate);
draft_request_mstates.push_back(draft_mstate);
generation_cfg.push_back(rsentries[i]->request->generation_cfg);
rngs.push_back(&rsentries[i]->rng);
draft_output_tokens.push_back(draft_mstate->draft_output_tokens);
}
Tensor draft_probs_on_device = models_[draft_model_id_]->GatherDraftProbs(
model_workspaces_[verify_model_id_].draft_probs_storage, draft_token_slots_,
&model_workspaces_[verify_model_id_].draft_probs);
std::vector<int> cum_verify_lengths = {0};
cum_verify_lengths.reserve(num_rsentries + 1);
std::vector<int> verify_lengths;
for (int i = 0; i < num_rsentries; ++i) {
// Add one committed token.
verify_lengths.push_back(draft_lengths[i] + 1);
cum_verify_lengths.push_back(cum_verify_lengths.back() + verify_lengths.back());
}
RECORD_EVENT(trace_recorder_, request_ids, "start verify embedding");
ObjectRef embeddings = models_[verify_model_id_]->TokenEmbed(
{Shape{all_tokens_to_verify.begin(), all_tokens_to_verify.end()}});
RECORD_EVENT(trace_recorder_, request_ids, "finish verify embedding");
RECORD_EVENT(trace_recorder_, request_ids, "start verify");
ObjectRef hidden_states = models_[verify_model_id_]->BatchVerifyToLastHidden(
embeddings, request_internal_ids, verify_lengths, token_tree_parent_ptr);
Tensor logits = models_[verify_model_id_]->GetLogits(hidden_states);
RECORD_EVENT(trace_recorder_, request_ids, "finish verify");
TVM_FFI_ICHECK_EQ(logits->ndim, 2);
TVM_FFI_ICHECK_EQ(logits->shape[0], cum_verify_lengths.back());
// - Update logits.
logit_processor_->InplaceUpdateLogits(logits, generation_cfg, verify_request_mstates,
request_ids, &cum_verify_lengths, &draft_request_mstates,
&draft_token_indices);
// - Compute probability distributions.
Tensor probs_on_device = logit_processor_->ComputeProbsFromLogits(
logits, generation_cfg, request_ids, &cum_verify_lengths);
// - Commit the prefix cache changes from previous round of action.
// Note: we commit prefix cache changes here to overlap this commit with the GPU execution.
estate->prefix_cache->CommitSequenceExtention();
std::vector<int> sample_indices(num_rsentries);
std::iota(sample_indices.begin(), sample_indices.end(), 0);
Tensor renormalized_probs = sampler_->BatchRenormalizeProbsByTopP(
probs_on_device, sample_indices, request_ids, generation_cfg);
auto [sample_results_arr, _] = sampler_->BatchVerifyDraftTokensWithProbAfterTopP(
renormalized_probs, request_ids, cum_verify_lengths, generation_cfg, rngs,
draft_output_tokens, token_tree_parent_ptr, draft_probs_on_device);
TVM_FFI_ICHECK_EQ(sample_results_arr.size(), num_rsentries);
// We collect the requests whose drafts are fully accepted.
// When a request's draft is fully accepted, there is an extra token proposed
// by the draft model but not added into the draft model's KV cache.
// In this case, an additional batch decode step is needed for these requests.
std::vector<int64_t> fully_accepted_rsentries;
std::vector<int64_t> verify_model_seq_internal_ids;
std::vector<int64_t> accepted_token_tree_leaf_nodes;
fully_accepted_rsentries.reserve(num_rsentries);
verify_model_seq_internal_ids.reserve(num_rsentries);
accepted_token_tree_leaf_nodes.reserve(num_rsentries);
std::vector<int> last_accepted_hidden_positions;
last_accepted_hidden_positions.reserve(num_rsentries);
for (int i = 0; i < num_rsentries; ++i) {
const std::vector<SampleResult>& sample_results = sample_results_arr[i];
int accept_length = sample_results.size();
TVM_FFI_ICHECK_GE(accept_length, 1);
for (SampleResult sample_result : sample_results) {
rsentries[i]->mstates[verify_model_id_]->CommitToken(sample_result);
rsentries[i]->mstates[draft_model_id_]->CommitToken(sample_result);
}
// Metrics update
// live update the output metrics
rsentries[i]->rstate->metrics.completion_tokens += accept_length;
rsentries[i]->rstate->metrics.decode_tokens += accept_length;
estate->metrics.spec_decode.Update(cum_verify_lengths[i + 1] - cum_verify_lengths[i],
accept_length);
// - Minus one because the last draft token has no kv cache entry
// - Take max with 0 in case of all accepted.
int rollback_length =
std::max(cum_verify_lengths[i + 1] - cum_verify_lengths[i] - accept_length, 0);
// Commit accepted tokens to the "verify_model", rollback kv cache
// in the "draft_model".
// NOTE: when number of small models is more than 1 (in the future),
// it is possible to re-compute prefill for the small models.
verify_model_seq_internal_ids.push_back(rsentries[i]->mstates[verify_model_id_]->internal_id);
accepted_token_tree_leaf_nodes.push_back(accept_length - 1);
if (rollback_length > 0) {
// Draft model rollback minus one because verify uses one more token.
models_[draft_model_id_]->PopNFromKVCache(
rsentries[i]->mstates[draft_model_id_]->internal_id, rollback_length - 1);
} else {
fully_accepted_rsentries.push_back(i);
}
// clear the draft model state entries
rsentries[i]->mstates[draft_model_id_]->RemoveAllDraftTokens(&draft_token_slots_);
draft_token_workspace_manager_->FreeSlots(draft_token_slots_);
// - Slice and save hidden_states_for_sample
last_accepted_hidden_positions.push_back(cum_verify_lengths[i] + accept_length - 1);
}
models_[verify_model_id_]->CommitAcceptedTokenTreeNodesToKVCache(
verify_model_seq_internal_ids, accepted_token_tree_leaf_nodes);
if (!fully_accepted_rsentries.empty() &&
engine_config_->speculative_mode == SpeculativeMode::kEagle) {
// - Run a step of batch decode for requests whose drafts are fully accepted.
// When a request's draft is fully accepted, there is an extra token proposed
// by the draft model but not added into the draft model's KV cache.
// In this case, an additional batch decode step is needed for these requests.
std::vector<int> input_tokens;
std::vector<int64_t> fully_accepted_request_internal_ids;
input_tokens.reserve(fully_accepted_rsentries.size());
fully_accepted_request_internal_ids.reserve(fully_accepted_rsentries.size());
std::vector<int> hidden_states_positions_for_fully_accepted;
hidden_states_positions_for_fully_accepted.reserve(fully_accepted_rsentries.size());
for (int rsentry_id : fully_accepted_rsentries) {
int num_committed_tokens =
rsentries[rsentry_id]->mstates[verify_model_id_]->committed_tokens.size();
// When a request's draft is fully accepted, an additional new token is sampled.
// So the token needed to fill in the draft model is the committed_token[-2].
TVM_FFI_ICHECK_GE(num_committed_tokens, 2);
input_tokens.push_back(rsentries[rsentry_id]
->mstates[verify_model_id_]
->committed_tokens[num_committed_tokens - 2]
.GetTokenId());
// Taking the hidden states of the token before the last token
hidden_states_positions_for_fully_accepted.push_back(
last_accepted_hidden_positions[rsentry_id] - 1);
fully_accepted_request_internal_ids.push_back(
rsentries[rsentry_id]->mstates[draft_model_id_]->internal_id);
}
// - Compute embeddings.
ObjectRef embeddings =
models_[draft_model_id_]->TokenEmbed({Shape{input_tokens.begin(), input_tokens.end()}});
// - Gather hidden states
ObjectRef hidden_states_for_fully_accepted = models_[draft_model_id_]->GatherHiddenStates(
hidden_states, hidden_states_positions_for_fully_accepted,
&model_workspaces_[draft_model_id_].hidden_states);
// - Invoke model decode.
ObjectRef fused_embedding_hidden_states = models_[draft_model_id_]->FuseEmbedHidden(
embeddings, hidden_states_for_fully_accepted,
/*batch_size*/ fully_accepted_rsentries.size(), /*seq_len*/ 1);
hidden_states_for_fully_accepted = models_[draft_model_id_]->BatchDecodeToLastHidden(
fused_embedding_hidden_states, fully_accepted_request_internal_ids);
// - We explicitly synchronize to avoid the input tokens getting overriden in the
// next runs of BatchDecode.
// This is because we do not do sample for this round of batch decode.
if (hidden_states_for_fully_accepted->IsInstance<DRefObj>()) {
(hidden_states_for_fully_accepted.as_or_throw<DRef>()->session)
.as_or_throw<Session>()
->SyncWorker(0);
} else {
Tensor hidden_states_for_fully_accepted_nd =
hidden_states_for_fully_accepted.as_or_throw<Tensor>();
DeviceAPI::Get(hidden_states_for_fully_accepted_nd->device)
->StreamSync(hidden_states_for_fully_accepted_nd->device, nullptr);
}
}
{
// One step draft for the following steps
// Gather hidden states for the last accepted tokens.
// Use the function and the workspace of the verify model because the information about the
// hidden states is not available in the draft model for medusa.
hidden_states = models_[0]->GatherHiddenStates(hidden_states, last_accepted_hidden_positions,
&model_workspaces_[0].hidden_states);
std::vector<int> input_tokens;
Array<RequestModelState> mstates;
input_tokens.reserve(num_rsentries);
mstates.reserve(num_rsentries);
for (const RequestStateEntry& rsentry : rsentries) {
mstates.push_back(rsentry->mstates[draft_model_id_]);
}
for (int i = 0; i < num_rsentries; ++i) {
TVM_FFI_ICHECK(!mstates[i]->committed_tokens.empty());
input_tokens.push_back(mstates[i]->committed_tokens.back().GetTokenId());
}
Array<Tensor> multi_step_logits{nullptr}; // for medusa output
if (engine_config_->speculative_mode == SpeculativeMode::kEagle) {
// - Compute embeddings.
RECORD_EVENT(trace_recorder_, request_ids, "start proposal embedding");
embeddings =
models_[draft_model_id_]->TokenEmbed({Shape{input_tokens.begin(), input_tokens.end()}});
RECORD_EVENT(trace_recorder_, request_ids, "finish proposal embedding");
// - Invoke model decode.
RECORD_EVENT(trace_recorder_, request_ids, "start proposal decode");
ObjectRef fused_embedding_hidden_states = models_[draft_model_id_]->FuseEmbedHidden(
embeddings, hidden_states, /*batch_size*/ num_rsentries, /*seq_len*/ 1);
hidden_states = models_[draft_model_id_]->BatchDecodeToLastHidden(
fused_embedding_hidden_states, request_internal_ids);
int lm_head_model_id = models_[draft_model_id_]->CanGetLogits() ? draft_model_id_ : 0;
logits = models_[lm_head_model_id]->GetLogits(hidden_states);
RECORD_EVENT(trace_recorder_, request_ids, "finish proposal decode");
TVM_FFI_ICHECK_EQ(logits->ndim, 2);
TVM_FFI_ICHECK_EQ(logits->shape[0], num_rsentries);
} else if (engine_config_->speculative_mode == SpeculativeMode::kMedusa) {
multi_step_logits = models_[draft_model_id_]->GetMultiStepLogits(hidden_states);
}
// Fill range [0, num_rsentries) into `sample_indices`.
std::vector<int> sample_indices(num_rsentries);
std::iota(sample_indices.begin(), sample_indices.end(), 0);
if (engine_config_->speculative_mode == SpeculativeMode::kEagle) {
const auto& [renormalized_probs, sample_results] = ApplyLogitProcessorAndSample(
logit_processor_, sampler_, logits, generation_cfg, request_ids, mstates, rngs,
sample_indices, generation_cfg, request_ids, sample_indices);
UpdateRequestStatesWithDraftProposals(mstates, sample_results, draft_model_id_,
renormalized_probs, hidden_states, estate);
} else if (engine_config_->speculative_mode == SpeculativeMode::kMedusa) {
TVM_FFI_ICHECK_NE(estate->spec_draft_length, 0);
for (int draft_id = 0; draft_id < estate->spec_draft_length; draft_id++) {
const auto& [renormalized_probs, sample_results] = ApplyLogitProcessorAndSample(
logit_processor_, sampler_, multi_step_logits[draft_id], generation_cfg, request_ids,
mstates, rngs, sample_indices, generation_cfg, request_ids, sample_indices);
UpdateRequestStatesWithDraftProposals(mstates, sample_results, draft_model_id_,
renormalized_probs, hidden_states, estate);
}
}
}
// reset num_tokens_for_next_decode
for (const RequestStateEntry& rsentry : rsentries) {
rsentry->mstates[verify_model_id_]->num_tokens_for_next_decode = 0;
rsentry->mstates[draft_model_id_]->num_tokens_for_next_decode = 0;
}
auto tend = std::chrono::high_resolution_clock::now();
double elapsed_time = static_cast<double>((tend - tstart).count()) / 1e9;
estate->metrics.engine_decode_time_sum += elapsed_time;
estate->metrics.UpdateVerifyTimeByBatchSize(cum_verify_lengths.back(), elapsed_time);
return estate->running_queue;
}
private:
struct DraftRequestStateEntries {
/*! \brief The request state entries to verify. */
Array<RequestStateEntry> draft_rsentries;
/*! \brief The draft length of each request state. */
std::vector<int> draft_lengths;
/*! \brief The total draft length. */
int total_draft_length;
};
/*!
* \brief Decide whether to run verify for the draft of each request.
* \param estate The engine state.
* \return The drafts to verify, together with their respective
* state and input length.
*/
DraftRequestStateEntries GetDraftsToVerify(EngineState estate) {
std::vector<int> draft_lengths;
int total_draft_length = 0;
int total_required_pages = 0;
int num_available_pages = models_[verify_model_id_]->GetNumAvailablePages();
// Preempt the request state entries that cannot fit the large model for verification.
std::vector<RequestStateEntry> running_rsentries = estate->GetRunningRequestStateEntries();
std::vector<int> num_page_requirement;
num_page_requirement.reserve(running_rsentries.size());
for (const RequestStateEntry& rsentry : running_rsentries) {
int draft_length = rsentry->mstates[draft_model_id_]->draft_output_tokens.size();
int num_require_pages = (draft_length + engine_config_->kv_cache_page_size - 1) /
engine_config_->kv_cache_page_size;
draft_lengths.push_back(draft_length);
num_page_requirement.push_back(num_require_pages);
total_draft_length += draft_length;
total_required_pages += num_require_pages;
}
while (!CanVerify(total_required_pages)) {
if (estate->prefix_cache->TryFreeMemory()) continue;
RequestStateEntry preempted = PreemptLastRunningRequestStateEntry(
estate, models_, draft_token_workspace_manager_, trace_recorder_);
if (preempted.same_as(running_rsentries.back())) {
total_draft_length -= draft_lengths.back();
total_required_pages -= num_page_requirement.back();
draft_lengths.pop_back();
num_page_requirement.pop_back();
running_rsentries.pop_back();
}
}
return {running_rsentries, draft_lengths, total_draft_length};
}
bool CanVerify(int num_required_pages) {
int num_available_pages = models_[0]->GetNumAvailablePages();
return num_required_pages <= num_available_pages;
}
void UpdateRequestStatesWithDraftProposals(const Array<RequestModelState>& mstates,
const std::vector<SampleResult>& sample_results,
int model_id, const Tensor& renormalized_probs,
const ObjectRef& hidden_states_for_sample,
EngineState estate) {
draft_token_workspace_manager_->AllocSlots(mstates.size(), &draft_token_slots_);
models_[0]->ScatterDraftProbs(renormalized_probs, draft_token_slots_,
&model_workspaces_[0].draft_probs_storage);
if (engine_config_->speculative_mode == SpeculativeMode::kEagle &&
estate->spec_draft_length > 1) {
models_[0]->ScatterHiddenStates(hidden_states_for_sample, draft_token_slots_,
&model_workspaces_[0].draft_hidden_states_storage);
}
for (int i = 0; i < static_cast<int>(mstates.size()); ++i) {
int64_t parent_idx = static_cast<int64_t>(mstates[i]->draft_output_tokens.size()) - 1;
mstates[i]->AddDraftToken(sample_results[i], draft_token_slots_[i], parent_idx);
}
}
/*!
* \brief The model to run decode in. When there are multiple
* models, the `Step` function of the created action will not take effect.
*/
Array<Model> models_;
/*! \brief The logit processor. */
LogitProcessor logit_processor_;
/*! \brief The sampler to sample new tokens. */
Sampler sampler_;
/*! \brief Workspace of each model. */
std::vector<ModelWorkspace> model_workspaces_;
/*! \brief The draft token workspace manager. */
DraftTokenWorkspaceManager draft_token_workspace_manager_;
/*! \brief The engine config. */
EngineConfig engine_config_;
/*! \brief Event trace recorder. */
Optional<EventTraceRecorder> trace_recorder_;
/*! \brief Random number generator. */
RandomGenerator& rng_;
/*! \brief The ids of verify/draft models. */
const int verify_model_id_ = 0;
const int draft_model_id_ = 1;
const float eps_ = 1e-5;
/*! \brief Temporary buffer to store the slots of the current draft tokens */
std::vector<int> draft_token_slots_;
};
EngineAction EngineAction::EagleBatchVerify(
Array<Model> models, LogitProcessor logit_processor, Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager, EngineConfig engine_config,
Optional<EventTraceRecorder> trace_recorder) {
return EngineAction(tvm::ffi::make_object<EagleBatchVerifyActionObj>(
std::move(models), std::move(logit_processor), std::move(sampler),
std::move(model_workspaces), std::move(draft_token_workspace_manager),
std::move(engine_config), std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
@@ -0,0 +1,502 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/eagle_new_request_prefill.cc
*/
#include <tvm/support/cuda/nvtx.h>
#include "../sampler/sampler.h"
#include "batch_prefill_base.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
/*!
* \brief The action that prefills requests in the `waiting_queue` of
* the engine state.
*/
class EagleNewRequestPrefillActionObj : public BatchPrefillBaseActionObj {
public:
explicit EagleNewRequestPrefillActionObj(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager,
EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder)
: BatchPrefillBaseActionObj(std::move(models), std::move(engine_config),
std::move(model_configs), std::move(trace_recorder)),
logit_processor_(std::move(logit_processor)),
sampler_(std::move(sampler)),
model_workspaces_(std::move(model_workspaces)),
draft_token_workspace_manager_(std::move(draft_token_workspace_manager)) {}
Array<Request> Step(EngineState estate) final {
// - Find the requests in `waiting_queue` that can prefill in this step.
std::vector<PrefillInput> prefill_inputs;
{
NVTXScopedRange nvtx_scope("NewRequestPrefill getting requests");
prefill_inputs = GetRequestStateEntriesToPrefill(estate);
if (prefill_inputs.empty()) {
return {};
}
}
int num_rsentries = prefill_inputs.size();
{
NVTXScopedRange nvtx_scope("NewRequestPrefill matching prefix");
for (int i = 0; i < num_rsentries; ++i) {
MatchPrefixCache(estate, &prefill_inputs[i]);
}
}
auto tstart = std::chrono::high_resolution_clock::now();
// - Update status of request states from pending to alive.
Array<String> request_ids;
std::vector<RequestState> rstates_of_entries;
std::vector<RequestStateStatus> status_before_prefill;
UpdateRequestToAlive(prefill_inputs, estate, &request_ids, &rstates_of_entries,
&status_before_prefill);
// - Get embedding and run prefill for each model.
std::vector<int> prefill_lengths;
prefill_lengths.resize(/*size=*/num_rsentries, /*value=*/-1);
ObjectRef hidden_states_for_input{nullptr};
ObjectRef hidden_states_for_sample{nullptr};
Tensor logits_for_sample{nullptr};
// A map used to record the entry and child_idx pair needed to fork sequence.
// The base model (id 0) should record all the pairs and all the small models
// fork sequences according to this map.
std::unordered_map<int, std::unordered_set<int>> fork_rsentry_child_map;
std::vector<bool> extra_prefill_tokens;
prefill_lengths.resize(/*size=*/num_rsentries, /*value=*/false);
for (int model_id = 0; model_id < static_cast<int>(models_.size()); ++model_id) {
std::vector<int64_t> request_internal_ids;
request_internal_ids.reserve(num_rsentries);
ObjectRef embeddings = model_workspaces_[model_id].embeddings;
int cum_prefill_length = 0;
bool single_input =
num_rsentries == 1 && prefill_inputs[0].rsentry->mstates[model_id]->inputs.size() == 1;
for (int i = 0; i < num_rsentries; ++i) {
const RequestStateEntry& rsentry = prefill_inputs[i].rsentry;
RequestModelState mstate = rsentry->mstates[model_id];
TVM_FFI_ICHECK(mstate->draft_output_tokens.empty());
TVM_FFI_ICHECK(mstate->draft_token_slots.empty());
if (status_before_prefill[i] == RequestStateStatus::kPending) {
if (!estate->prefix_cache->HasSequence(mstate->internal_id)) {
// Add the sequence to the model, or fork the sequence from its parent.
// If the sequence is already in prefix cache, it has also been added/forked in the
// KVCache.
if (rsentry->parent_idx == -1) {
models_[model_id]->AddNewSequence(mstate->internal_id);
} else {
models_[model_id]->ForkSequence(rstates_of_entries[i]
->entries[rsentry->parent_idx]
->mstates[model_id]
->internal_id,
mstate->internal_id);
}
}
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
models_[model_id]->EnableSlidingWindowForSeq(mstate->internal_id);
}
// Shift the input tokens by 1 for eagle models.
if (model_id == 0) {
for (int j = 1; j < static_cast<int>(models_.size()); ++j) {
TVM_FFI_ICHECK(rsentry->mstates[j]->inputs.size());
TokenData token_data = rsentry->mstates[j]->inputs[0].as_or_throw<TokenData>();
rsentry->mstates[j]->inputs.Set(0, TokenData(Shape(token_data->token_ids.begin() + 1,
token_data->token_ids.end())));
}
}
}
request_internal_ids.push_back(mstate->internal_id);
if (engine_config_->speculative_mode == SpeculativeMode::kMedusa && model_id > 0) {
// Embedding is only needed for the base model in Medusa.
continue;
}
auto [input_data, input_length] =
ChunkPrefillInputData(mstate, prefill_inputs[i].max_prefill_length);
if (prefill_lengths[i] == -1) {
prefill_lengths[i] = input_length;
} else {
TVM_FFI_ICHECK_EQ(prefill_lengths[i], input_length);
}
mstate->num_prefilled_tokens += input_length;
RECORD_EVENT(trace_recorder_, prefill_inputs[i].rsentry->request->id, "start embedding");
// Speculative models shift left the input tokens by 1 when base model has committed tokens.
// Note: for n > 1 cases Eagle doesn't work because parent entry doesn't shift input tokens.
for (int j = 0; j < static_cast<int>(input_data.size()); ++j) {
if (model_id == 0) {
mstate->prefilled_inputs.push_back(input_data[j]);
}
embeddings = input_data[j]->GetEmbedding(
models_[model_id],
/*dst=*/!single_input ? &model_workspaces_[model_id].embeddings : nullptr,
/*offset=*/cum_prefill_length);
cum_prefill_length += input_data[j]->GetLength();
}
RECORD_EVENT(trace_recorder_, rsentry->request->id, "finish embedding");
}
RECORD_EVENT(trace_recorder_, request_ids, "start prefill");
Array<Tensor> multi_step_logits{nullptr};
if (model_id == 0 || engine_config_->speculative_mode == SpeculativeMode::kEagle) {
ObjectRef embedding_or_hidden_states{nullptr};
if (model_id == 0) {
embedding_or_hidden_states = embeddings;
} else {
embedding_or_hidden_states =
models_[model_id]->FuseEmbedHidden(embeddings, hidden_states_for_input,
/*batch_size*/ 1, /*seq_len*/ cum_prefill_length);
}
// hidden_states: (b * s, h)
ObjectRef hidden_states = models_[model_id]->BatchPrefillToLastHidden(
embedding_or_hidden_states, request_internal_ids, prefill_lengths);
RECORD_EVENT(trace_recorder_, request_ids, "finish prefill");
if (model_id == 0) {
// We only need to sample for model 0 in prefill.
hidden_states_for_input = hidden_states;
// - Commit the prefix cache changes from previous round of action.
// Note: we commit prefix cache changes here to overlap this commit with the GPU
// execution.
estate->prefix_cache->CommitSequenceExtention();
}
// Whether to use base model to get logits.
int sample_model_id = !models_[model_id]->CanGetLogits() ? 0 : model_id;
std::vector<int> logit_positions;
{
// Prepare the logit positions
logit_positions.reserve(prefill_lengths.size());
int total_len = 0;
for (int i = 0; i < prefill_lengths.size(); ++i) {
total_len += prefill_lengths[i];
logit_positions.push_back(total_len - 1);
}
}
// hidden_states_for_sample: (b * s, h)
hidden_states_for_sample = models_[sample_model_id]->GatherHiddenStates(
hidden_states, logit_positions, &model_workspaces_[model_id].hidden_states);
// logits_for_sample: (b * s, v)
logits_for_sample = models_[sample_model_id]->GetLogits(hidden_states_for_sample);
} else if (engine_config_->speculative_mode == SpeculativeMode::kMedusa) {
// Note: spec_draft_length in engine config has to be match the model config in Medusa.
multi_step_logits = models_[model_id]->GetMultiStepLogits(hidden_states_for_sample);
} else {
LOG(FATAL) << "unreachable";
}
Array<String> child_request_ids;
// - Prepare the configurations for the sampler.
// For prefill_inputs which have children, sample
// one token for each rstate that is depending.
// Otherwise, sample a token for the current rstate.
std::vector<int> child_sample_indices;
std::vector<RequestStateEntry> rsentries_for_sample;
std::vector<RandomGenerator*> rngs;
std::vector<bool> rsentry_activated;
Array<GenerationConfig> child_generation_cfg;
child_sample_indices.reserve(num_rsentries);
child_generation_cfg.reserve(num_rsentries);
child_request_ids.reserve(num_rsentries);
rsentries_for_sample.reserve(num_rsentries);
rngs.reserve(num_rsentries);
rsentry_activated.reserve(num_rsentries);
for (int i = 0; i < num_rsentries; ++i) {
const RequestStateEntry& rsentry = prefill_inputs[i].rsentry;
// No sample for rsentries with remaining inputs.
if (!rsentry->mstates[0]->inputs.empty()) {
continue;
}
int remaining_num_child_to_activate = prefill_inputs[i].num_child_to_activate;
for (int child_idx : rsentry->child_indices) {
// Only use base model to judge if we need to add child entries.
if ((rstates_of_entries[i]->entries[child_idx]->status == RequestStateStatus::kPending &&
rstates_of_entries[i]
->entries[child_idx]
->mstates[0]
->committed_tokens.empty() ||
fork_rsentry_child_map[i].count(child_idx))) {
// If rstates_of_entries[i]->entries[child_idx] has no committed token,
// the prefill of the current rsentry will unblock
// rstates_of_entries[i]->entries[child_idx],
// and thus we want to sample a token for rstates_of_entries[i]->entries[child_idx].
fork_rsentry_child_map[i].insert(child_idx);
child_sample_indices.push_back(i);
rsentries_for_sample.push_back(rstates_of_entries[i]->entries[child_idx]);
child_request_ids.push_back(rsentry->request->id);
child_generation_cfg.push_back(rsentry->request->generation_cfg);
rngs.push_back(&rstates_of_entries[i]->entries[child_idx]->rng);
// We only fork the first `num_child_to_activate` children.
// The children not being forked will be forked via later prefills.
// Usually `num_child_to_activate` is the same as the number of children.
// But it can be fewer subject to the KV cache max num sequence limit.
if (remaining_num_child_to_activate == 0) {
rsentry_activated.push_back(false);
continue;
}
rsentry_activated.push_back(true);
--remaining_num_child_to_activate;
if (model_id == 0) {
TVM_FFI_ICHECK(rstates_of_entries[i]->entries[child_idx]->status ==
RequestStateStatus::kPending);
rstates_of_entries[i]->entries[child_idx]->status = RequestStateStatus::kAlive;
}
int64_t child_internal_id =
rstates_of_entries[i]->entries[child_idx]->mstates[model_id]->internal_id;
models_[model_id]->ForkSequence(rsentry->mstates[model_id]->internal_id,
child_internal_id);
// Enable sliding window for the child sequence if the child is not a parent.
if (rstates_of_entries[i]->entries[child_idx]->child_indices.empty()) {
models_[model_id]->EnableSlidingWindowForSeq(child_internal_id);
}
}
}
if (rsentry->child_indices.empty()) {
// If rsentry has no child, we sample a token for itself.
child_sample_indices.push_back(i);
rsentries_for_sample.push_back(rsentry);
child_request_ids.push_back(rsentry->request->id);
child_generation_cfg.push_back(rsentry->request->generation_cfg);
rngs.push_back(&rsentry->rng);
rsentry_activated.push_back(true);
}
}
// - Prepare input for logit processor.
TVM_FFI_ICHECK(logits_for_sample.defined());
Array<GenerationConfig> generation_cfg;
Array<RequestModelState> mstates_for_logitproc;
std::vector<int> sample_indices(num_rsentries);
generation_cfg.reserve(num_rsentries);
mstates_for_logitproc.reserve(num_rsentries);
std::iota(sample_indices.begin(), sample_indices.end(), 0);
for (int i = 0; i < num_rsentries; ++i) {
generation_cfg.push_back(prefill_inputs[i].rsentry->request->generation_cfg);
mstates_for_logitproc.push_back(prefill_inputs[i].rsentry->mstates[model_id]);
}
if (model_id == 0 || engine_config_->speculative_mode == SpeculativeMode::kEagle) {
const auto& [renormalized_probs, sample_results] = ApplyLogitProcessorAndSample(
logit_processor_, sampler_, logits_for_sample, generation_cfg, request_ids,
mstates_for_logitproc, rngs, sample_indices, child_generation_cfg, child_request_ids,
child_sample_indices);
if (model_id == 0) {
UpdateRequestStateEntriesWithSampleResults(rsentries_for_sample, rsentry_activated,
sample_results);
// Add the sampled token as an input of the eagle models.
if (engine_config_->speculative_mode == SpeculativeMode::kEagle) {
for (int i = 0; i < static_cast<int>(rsentries_for_sample.size()); ++i) {
for (int mid = 1; mid < static_cast<int>(models_.size()); ++mid) {
TokenData token_data =
rsentries_for_sample[i]->mstates[mid]->inputs.back().as_or_throw<TokenData>();
std::vector<int32_t> token_ids = {token_data->token_ids.begin(),
token_data->token_ids.end()};
token_ids.push_back(sample_results[i].GetTokenId());
int ninputs =
static_cast<int>(rsentries_for_sample[i]->mstates[mid]->inputs.size());
rsentries_for_sample[i]->mstates[mid]->inputs.Set(
ninputs - 1, TokenData(Shape(token_ids.begin(), token_ids.end())));
}
}
}
} else {
// - Slice and save hidden_states_for_sample
UpdateRequestStatesWithDraftProposals(rsentries_for_sample, sample_results, model_id,
renormalized_probs, hidden_states_for_sample,
estate, child_sample_indices);
}
} else if (engine_config_->speculative_mode == SpeculativeMode::kMedusa) {
TVM_FFI_ICHECK_NE(estate->spec_draft_length, 0);
for (int draft_id = 0; draft_id < estate->spec_draft_length; ++draft_id) {
const auto& [renormalized_probs, sample_results] = ApplyLogitProcessorAndSample(
logit_processor_, sampler_, multi_step_logits[draft_id], generation_cfg, request_ids,
mstates_for_logitproc, rngs, sample_indices, child_generation_cfg, child_request_ids,
child_sample_indices);
UpdateRequestStatesWithDraftProposals(
rsentries_for_sample, sample_results, model_id, renormalized_probs,
/*hidden_states=*/ObjectRef{nullptr}, estate, child_sample_indices);
}
}
}
auto tend = std::chrono::high_resolution_clock::now();
estate->metrics.engine_prefill_time_sum += static_cast<double>((tend - tstart).count()) / 1e9;
std::vector<Request> processed_requests =
RemoveProcessedRequests(prefill_inputs, estate, rstates_of_entries);
estate->running_rsentries_changed = true;
return processed_requests;
}
void UpdateRequestStatesWithDraftProposals(
const std::vector<RequestStateEntry>& rsentries_for_sample,
const std::vector<SampleResult>& sample_results, int model_id,
const Tensor& renormalized_probs, const ObjectRef& hidden_states_for_sample,
EngineState estate, const std::vector<int>& sample_indices) {
std::vector<int> reuse_count(renormalized_probs->shape[0], 0);
for (int i = 0; i < static_cast<int>(sample_indices.size()); ++i) {
// The same probability may be sampled multiple times.
reuse_count[sample_indices[i]]++;
}
draft_token_workspace_manager_->AllocSlots(renormalized_probs->shape[0], reuse_count,
&draft_token_slots_);
models_[0]->ScatterDraftProbs(renormalized_probs, draft_token_slots_,
&model_workspaces_[0].draft_probs_storage);
if (engine_config_->speculative_mode == SpeculativeMode::kEagle &&
estate->spec_draft_length > 1) {
models_[0]->ScatterHiddenStates(hidden_states_for_sample, draft_token_slots_,
&model_workspaces_[0].draft_hidden_states_storage);
}
for (int i = 0; i < static_cast<int>(rsentries_for_sample.size()); ++i) {
int parent_idx =
rsentries_for_sample[i]->mstates[model_id]->draft_output_tokens.empty()
? -1
: rsentries_for_sample[i]->mstates[model_id]->draft_output_tokens.size() - 1;
rsentries_for_sample[i]->mstates[model_id]->AddDraftToken(
sample_results[i], draft_token_slots_[sample_indices[i]], parent_idx);
}
}
private:
/*! \brief The logit processor. */
LogitProcessor logit_processor_;
/*! \brief The sampler to sample new tokens. */
Sampler sampler_;
/*! \brief Workspace of each model. */
std::vector<ModelWorkspace> model_workspaces_;
/*! \brief The draft token workspace manager. */
DraftTokenWorkspaceManager draft_token_workspace_manager_;
/*! \brief Temporary buffer to store the slots of the current draft tokens */
std::vector<int> draft_token_slots_;
/*!
* \brief Match the request state entry with prefix cache, to skip prefilling common prefix
* tokens. If the request state entry is not added to KVCache yet, this method will add/fork the
* request in the KVCache, depending on the matching result from prefix cache.
* \param estate The engine state.
* \param[in, out] input The prefill input to be matched and updated.
*/
int MatchPrefixCache(EngineState estate, PrefillInput* input) final {
RequestStateEntry rsentry = input->rsentry;
if (estate->prefix_cache->Mode() == PrefixCacheMode::kDisable) {
return 0;
}
if (rsentry->parent_idx == -1 && rsentry->status == RequestStateStatus::kPending &&
!estate->prefix_cache->HasSequence(rsentry->mstates[0]->internal_id)) {
std::vector<int32_t> tokens = GetConcatPrefillInputData(rsentry->mstates[0]);
if (tokens.empty()) {
// If the RequestStateEntry is of empty input data, or not fully tokenized, do nothing
// and return.
return 0;
}
PrefixCacheMatchedResult result = estate->prefix_cache->InsertSequence(
rsentry->mstates[0]->internal_id, tokens, models_[0]->GetSlidingWindowSize(),
models_[0]->GetAttentionSinkSize());
if (result.prefilled_offset == 0) {
// Add new sequence.
// Note: Almost same as without eagle speculative decoding. But in prefill step, the
// prefill embedding input in draft model will be shifted one token, compared to the base
// model. Just the new sequence without prefix cache. Here we merely add the new sequence
// in advance of prefill step.
TVM_FFI_ICHECK_EQ(result.forked_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_pop_last_tokens, 0);
for (int i = 0; i < models_.size(); ++i) {
models_[i]->AddNewSequence(rsentry->mstates[0]->internal_id);
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
models_[i]->EnableSlidingWindowForSeq(rsentry->mstates[0]->internal_id);
}
}
} else {
if (result.forked_seq_id != -1) {
// Fork from active sequence
// Note: Due to the shifted KVCache between base model and draft model, we do a trick
// over forking sequence:
// For example. we have a sequence of [0, 1, 2] in base model KVCache, and the
// corresponding sequence of [1, 2, 3] in draft model KVCache, where token [3] was
// sampled from base model, but not appended in base model KVCache. Then we get a new
// sequence [0, 1, 4] to prefill. Although the new sequence matches first two tokens
// with the sequence [0, 1, 2], we have to fork from the first token 0, not the second
// token 1. Because if we fork from the second token, we will prefill like: Base model:
// [0, 1] + prefill([4]) => [5] Draft model: [1] + prefill([4, 5]) The lengths to
// prefill is different between base model and draft model, which is illegal. So we roll
// back one token in prefix cache to fork from the first token. Then the prefill will be
// like: Base model: [0] + prefill([1, 4]) => [5] Draft model: [1] + prefill([4, 5]) And
// we shift the input prefill data as other new sequence, to avoid double prefilling
// token 1, and make the prefill length aligned between base model and draft model.
TVM_FFI_ICHECK_EQ(result.reused_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_pop_last_tokens, 0);
estate->prefix_cache->RollBackSequence(rsentry->mstates[0]->internal_id, 1);
for (int i = 0; i < models_.size(); ++i) {
models_[i]->ForkSequence(result.forked_seq_id, rsentry->mstates[0]->internal_id,
result.prefilled_offset - 1);
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
models_[i]->EnableSlidingWindowForSeq(rsentry->mstates[0]->internal_id);
}
}
} else {
// Reuse recycling sequence
// Note: The processing for reusing recycling sequence is like forking sequence. And we
// also roll back one token due to the reason mentioned above.
TVM_FFI_ICHECK_EQ(result.forked_seq_id, -1);
estate->id_manager.RecycleId(rsentry->mstates[0]->internal_id);
for (int i = 0; i < rsentry->mstates.size(); ++i) {
rsentry->mstates[i]->internal_id = result.reused_seq_id;
}
estate->prefix_cache->RollBackSequence(rsentry->mstates[0]->internal_id, 1);
for (int i = 0; i < models_.size(); ++i) {
models_[i]->PopNFromKVCache(rsentry->mstates[0]->internal_id,
result.reused_seq_pop_last_tokens + 1);
}
result.prefilled_offset -= 1;
}
}
// Pop matched prefix
if (result.prefilled_offset > 0) {
for (int i = 0; i < rsentry->mstates.size(); ++i) {
PopPrefillInputData(rsentry->mstates[i], result.prefilled_offset);
}
}
// Update max prefill length
input->max_prefill_length =
std::min(input->max_prefill_length, rsentry->mstates[0]->GetInputLength());
return result.prefilled_offset - 1;
}
return 0;
}
};
EngineAction EngineAction::EagleNewRequestPrefill(
Array<Model> models, LogitProcessor logit_processor, Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
DraftTokenWorkspaceManager draft_token_workspace_manager, EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder) {
return EngineAction(tvm::ffi::make_object<EagleNewRequestPrefillActionObj>(
std::move(models), std::move(logit_processor), std::move(sampler),
std::move(model_workspaces), std::move(draft_token_workspace_manager),
std::move(engine_config), std::move(model_configs), std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
@@ -0,0 +1,370 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_actions/new_request_prefill.cc
*/
#include <tvm/support/cuda/nvtx.h>
#include "../sampler/sampler.h"
#include "batch_prefill_base.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
/*!
* \brief The action that prefills requests in the `waiting_queue` of
* the engine state.
*/
class NewRequestPrefillActionObj : public BatchPrefillBaseActionObj {
public:
explicit NewRequestPrefillActionObj(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler, std::vector<ModelWorkspace> model_workspaces,
EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder)
: BatchPrefillBaseActionObj(std::move(models), std::move(engine_config),
std::move(model_configs), std::move(trace_recorder)),
logit_processor_(std::move(logit_processor)),
sampler_(std::move(sampler)),
model_workspaces_(std::move(model_workspaces)) {}
Array<Request> Step(EngineState estate) final {
// - Find the requests in `waiting_queue` that can prefill in this step.
std::vector<PrefillInput> prefill_inputs;
{
NVTXScopedRange nvtx_scope("NewRequestPrefill getting requests");
prefill_inputs = GetRequestStateEntriesToPrefill(estate);
if (prefill_inputs.empty()) {
return {};
}
}
int num_rsentries = prefill_inputs.size();
{
NVTXScopedRange nvtx_scope("NewRequestPrefill matching prefix");
for (int i = 0; i < num_rsentries; ++i) {
MatchPrefixCache(estate, &prefill_inputs[i]);
}
}
auto tstart = std::chrono::high_resolution_clock::now();
// - Update status of request states from pending to alive.
Array<String> request_ids;
std::vector<RequestState> rstates_of_entries;
std::vector<RequestStateStatus> status_before_prefill;
UpdateRequestToAlive(prefill_inputs, estate, &request_ids, &rstates_of_entries,
&status_before_prefill);
// - Get embedding and run prefill for each model.
std::vector<int> prefill_lengths;
prefill_lengths.resize(/*size=*/num_rsentries, /*value=*/-1);
Tensor logits_for_sample{nullptr};
for (int model_id = 0; model_id < static_cast<int>(models_.size()); ++model_id) {
std::vector<int64_t> request_internal_ids;
request_internal_ids.reserve(num_rsentries);
ObjectRef embeddings = model_workspaces_[model_id].embeddings;
int cum_prefill_length = 0;
bool single_input =
num_rsentries == 1 && prefill_inputs[0].rsentry->mstates[model_id]->inputs.size() == 1;
std::vector<int64_t> cached_token_data;
for (int i = 0; i < num_rsentries; ++i) {
const RequestStateEntry& rsentry = prefill_inputs[i].rsentry;
RequestModelState mstate = rsentry->mstates[model_id];
auto [input_data, input_length] =
ChunkPrefillInputData(mstate, prefill_inputs[i].max_prefill_length);
if (prefill_lengths[i] == -1) {
prefill_lengths[i] = input_length;
} else {
TVM_FFI_ICHECK_EQ(prefill_lengths[i], input_length);
}
mstate->num_prefilled_tokens += input_length;
TVM_FFI_ICHECK(mstate->draft_output_tokens.empty());
TVM_FFI_ICHECK(mstate->draft_token_slots.empty());
if (status_before_prefill[i] == RequestStateStatus::kPending &&
!estate->prefix_cache->HasSequence(mstate->internal_id)) {
// Add the sequence to the model, or fork the sequence from its parent.
// If the sequence is already in prefix cache, it has also been added/forked in the
// KVCache.
if (rsentry->parent_idx == -1) {
models_[model_id]->AddNewSequence(mstate->internal_id);
} else {
models_[model_id]->ForkSequence(
rstates_of_entries[i]->entries[rsentry->parent_idx]->mstates[model_id]->internal_id,
mstate->internal_id);
}
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
models_[model_id]->EnableSlidingWindowForSeq(mstate->internal_id);
}
}
request_internal_ids.push_back(mstate->internal_id);
RECORD_EVENT(trace_recorder_, rsentry->request->id, "start embedding");
for (int j = 0; j < static_cast<int>(input_data.size()); ++j) {
if (!model_id && !prefill_inputs[i].is_decode) {
mstate->prefilled_inputs.push_back(input_data[j]);
}
if (const auto* token_data = input_data[j].as<TokenDataNode>()) {
cached_token_data.insert(cached_token_data.end(), token_data->token_ids.begin(),
token_data->token_ids.end());
} else {
if (!cached_token_data.empty()) {
embeddings = TokenData(cached_token_data)
->GetEmbedding(models_[model_id],
/*dst=*/!single_input ? &embeddings : nullptr,
/*offset=*/cum_prefill_length);
cum_prefill_length += cached_token_data.size();
cached_token_data.clear();
}
embeddings = input_data[j]->GetEmbedding(models_[model_id],
/*dst=*/!single_input ? &embeddings : nullptr,
/*offset=*/cum_prefill_length);
cum_prefill_length += input_data[j]->GetLength();
}
}
RECORD_EVENT(trace_recorder_, rsentry->request->id, "finish embedding");
}
if (!cached_token_data.empty()) {
embeddings = TokenData(cached_token_data)
->GetEmbedding(models_[model_id],
/*dst=*/!single_input ? &embeddings : nullptr,
/*offset=*/cum_prefill_length);
cum_prefill_length += cached_token_data.size();
cached_token_data.clear();
}
RECORD_EVENT(trace_recorder_, request_ids, "start prefill");
Tensor logits =
models_[model_id]->BatchPrefill(embeddings, request_internal_ids, prefill_lengths);
RECORD_EVENT(trace_recorder_, request_ids, "finish prefill");
TVM_FFI_ICHECK_EQ(logits->ndim, 3);
TVM_FFI_ICHECK_EQ(logits->shape[0], 1);
TVM_FFI_ICHECK_EQ(logits->shape[1], num_rsentries);
if (model_id == 0) {
// We only need to sample for model 0 in prefill.
logits_for_sample = logits;
}
}
// - Update logits.
TVM_FFI_ICHECK(logits_for_sample.defined());
Array<GenerationConfig> generation_cfg;
Array<RequestModelState> mstates_for_logitproc;
generation_cfg.reserve(num_rsentries);
mstates_for_logitproc.reserve(num_rsentries);
for (int i = 0; i < num_rsentries; ++i) {
generation_cfg.push_back(prefill_inputs[i].rsentry->request->generation_cfg);
mstates_for_logitproc.push_back(prefill_inputs[i].rsentry->mstates[0]);
}
logits_for_sample = logits_for_sample.CreateView({num_rsentries, logits_for_sample->shape[2]},
logits_for_sample->dtype);
logit_processor_->InplaceUpdateLogits(logits_for_sample, generation_cfg, mstates_for_logitproc,
request_ids);
// - Compute probability distributions.
Tensor probs_on_device =
logit_processor_->ComputeProbsFromLogits(logits_for_sample, generation_cfg, request_ids);
// - Commit the prefix cache changes from previous round of action.
// Note: we commit prefix cache changes here to overlap this commit with the GPU execution.
estate->prefix_cache->CommitSequenceExtention();
// - Sample tokens.
// For rsentries which have children, sample
// one token for each rstate that is depending.
// Otherwise, sample a token for the current rstate.
std::vector<int> sample_indices;
std::vector<RequestStateEntry> rsentries_for_sample;
std::vector<RandomGenerator*> rngs;
std::vector<bool> rsentry_activated;
sample_indices.reserve(num_rsentries);
rsentries_for_sample.reserve(num_rsentries);
rngs.reserve(num_rsentries);
rsentry_activated.reserve(num_rsentries);
request_ids.clear();
generation_cfg.clear();
for (int i = 0; i < num_rsentries; ++i) {
const RequestStateEntry& rsentry = prefill_inputs[i].rsentry;
// No sample for rsentries with remaining inputs.
if (!rsentry->mstates[0]->inputs.empty()) {
continue;
}
int remaining_num_child_to_activate = prefill_inputs[i].num_child_to_activate;
for (int child_idx : rsentry->child_indices) {
// If rstates_of_entries[i]->entries[child_idx] has no committed token,
// the prefill of the current rsentry will unblock
// rstates_of_entries[i]->entries[child_idx],
// and thus we want to sample a token for rstates_of_entries[i]->entries[child_idx].
if (rstates_of_entries[i]->entries[child_idx]->status != RequestStateStatus::kPending ||
!rstates_of_entries[i]->entries[child_idx]->mstates[0]->committed_tokens.empty()) {
continue;
}
sample_indices.push_back(i);
rsentries_for_sample.push_back(rstates_of_entries[i]->entries[child_idx]);
request_ids.push_back(rsentry->request->id);
generation_cfg.push_back(rsentry->request->generation_cfg);
rngs.push_back(&rstates_of_entries[i]->entries[child_idx]->rng);
TVM_FFI_ICHECK(rstates_of_entries[i]->entries[child_idx]->status ==
RequestStateStatus::kPending);
// We only fork the first `num_child_to_activate` children.
// The children not being forked will be forked via later prefills.
// Usually `num_child_to_activate` is the same as the number of children.
// But it can be fewer subject to the KV cache max num sequence limit.
if (remaining_num_child_to_activate == 0) {
rsentry_activated.push_back(false);
continue;
}
rsentry_activated.push_back(true);
--remaining_num_child_to_activate;
rstates_of_entries[i]->entries[child_idx]->status = RequestStateStatus::kAlive;
for (int model_id = 0; model_id < static_cast<int>(models_.size()); ++model_id) {
int64_t child_internal_id =
rstates_of_entries[i]->entries[child_idx]->mstates[model_id]->internal_id;
models_[model_id]->ForkSequence(rsentry->mstates[model_id]->internal_id,
child_internal_id);
// Enable sliding window for the child sequence if the child is not a parent.
if (rstates_of_entries[i]->entries[child_idx]->child_indices.empty()) {
models_[model_id]->EnableSlidingWindowForSeq(child_internal_id);
}
}
}
if (rsentry->child_indices.empty()) {
// If rsentry has no child, we sample a token for itself.
sample_indices.push_back(i);
rsentries_for_sample.push_back(rsentry);
request_ids.push_back(rsentry->request->id);
generation_cfg.push_back(rsentry->request->generation_cfg);
rngs.push_back(&rsentry->rng);
rsentry_activated.push_back(true);
}
}
Tensor renormalized_probs = sampler_->BatchRenormalizeProbsByTopP(
probs_on_device, sample_indices, request_ids, generation_cfg);
std::vector<SampleResult> sample_results = sampler_->BatchSampleTokensWithProbAfterTopP(
renormalized_probs, sample_indices, request_ids, generation_cfg, rngs);
TVM_FFI_ICHECK_EQ(sample_results.size(), rsentries_for_sample.size());
// - Update the committed tokens of states.
// - If a request is first-time prefilled, set the prefill finish time.
UpdateRequestStateEntriesWithSampleResults(rsentries_for_sample, rsentry_activated,
sample_results);
auto tend = std::chrono::high_resolution_clock::now();
estate->metrics.engine_prefill_time_sum += static_cast<double>((tend - tstart).count()) / 1e9;
std::vector<Request> processed_requests =
RemoveProcessedRequests(prefill_inputs, estate, rstates_of_entries);
estate->running_rsentries_changed = true;
return processed_requests;
}
private:
/*! \brief The logit processor. */
LogitProcessor logit_processor_;
/*! \brief The sampler to sample new tokens. */
Sampler sampler_;
/*! \brief Workspace of each model. */
std::vector<ModelWorkspace> model_workspaces_;
/*!
* \brief Match the request state entry with prefix cache, to skip prefilling common prefix
* tokens. If the request state entry is not added to KVCache yet, this method will add/fork the
* request in the KVCache, depending on the matching result from prefix cache.
* \param estate The engine state.
* \param[in, out] input The prefill input to be matched and updated.
* \return The matched length in prefix cache.
*/
int MatchPrefixCache(EngineState estate, PrefillInput* input) final {
RequestStateEntry rsentry = input->rsentry;
if (estate->prefix_cache->Mode() == PrefixCacheMode::kDisable) {
return 0;
}
if (rsentry->parent_idx == -1 && rsentry->status == RequestStateStatus::kPending &&
!estate->prefix_cache->HasSequence(rsentry->mstates[0]->internal_id)) {
std::vector<int32_t> tokens = GetConcatPrefillInputData(rsentry->mstates[0]);
if (tokens.empty()) {
// If the RequestStateEntry is of empty input data, or not fully tokenized, do nothing
// and return.
return 0;
}
PrefixCacheMatchedResult result = estate->prefix_cache->InsertSequence(
rsentry->mstates[0]->internal_id, tokens, models_[0]->GetSlidingWindowSize(),
models_[0]->GetAttentionSinkSize());
if (result.prefilled_offset == 0) {
// Add new sequence
TVM_FFI_ICHECK_EQ(result.forked_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_pop_last_tokens, 0);
for (Model model : models_) {
model->AddNewSequence(rsentry->mstates[0]->internal_id);
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
model->EnableSlidingWindowForSeq(rsentry->mstates[0]->internal_id);
}
}
} else {
if (result.forked_seq_id != -1) {
TVM_FFI_ICHECK_EQ(result.reused_seq_id, -1);
TVM_FFI_ICHECK_EQ(result.reused_seq_pop_last_tokens, 0);
// Fork from active sequence
for (Model model : models_) {
model->ForkSequence(result.forked_seq_id, rsentry->mstates[0]->internal_id,
result.prefilled_offset);
// Enable sliding window for the sequence if it is not a parent.
if (rsentry->child_indices.empty()) {
model->EnableSlidingWindowForSeq(rsentry->mstates[0]->internal_id);
}
}
} else {
// Reuse recycling sequence
TVM_FFI_ICHECK_EQ(result.forked_seq_id, -1);
estate->id_manager.RecycleId(rsentry->mstates[0]->internal_id);
for (int i = 0; i < rsentry->mstates.size(); ++i) {
rsentry->mstates[i]->internal_id = result.reused_seq_id;
}
if (result.reused_seq_pop_last_tokens > 0) {
for (Model model : models_) {
model->PopNFromKVCache(rsentry->mstates[0]->internal_id,
result.reused_seq_pop_last_tokens);
}
}
}
}
// Pop matched prefix
if (result.prefilled_offset) {
for (int i = 0; i < rsentry->mstates.size(); ++i) {
PopPrefillInputData(rsentry->mstates[i], result.prefilled_offset);
}
}
// Update max prefill length
input->max_prefill_length =
std::min(input->max_prefill_length, rsentry->mstates[0]->GetInputLength());
return result.prefilled_offset;
}
return 0;
}
}; // namespace serve
EngineAction EngineAction::NewRequestPrefill(Array<Model> models, LogitProcessor logit_processor,
Sampler sampler,
std::vector<ModelWorkspace> model_workspaces,
EngineConfig engine_config,
std::vector<tvm::ffi::json::Object> model_configs,
Optional<EventTraceRecorder> trace_recorder) {
return EngineAction(tvm::ffi::make_object<NewRequestPrefillActionObj>(
std::move(models), std::move(logit_processor), std::move(sampler),
std::move(model_workspaces), std::move(engine_config), std::move(model_configs),
std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
+54
View File
@@ -0,0 +1,54 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_state.cc
*/
#include "engine_state.h"
namespace mlc {
namespace llm {
namespace serve {
TVM_FFI_STATIC_INIT_BLOCK() { EngineStateObj::RegisterReflection(); }
EngineState::EngineState() { data_ = tvm::ffi::make_object<EngineStateObj>(); }
void EngineStateObj::Reset() {
running_queue.clear();
waiting_queue.clear();
request_states.clear();
id_manager.Reset();
metrics.Reset();
if (prefix_cache.defined()) {
prefix_cache->Reset();
}
running_rsentries_changed = true;
postproc_workspace = ActionPostProcessWorkspace();
}
RequestState EngineStateObj::GetRequestState(Request request) {
TVM_FFI_ICHECK(request->rstate != nullptr) << "The state of the request has not been defined.";
return GetRef<RequestState>(static_cast<RequestStateNode*>(request->rstate));
}
const std::vector<RequestStateEntry>& EngineStateObj::GetRunningRequestStateEntries() {
if (running_rsentries_changed) {
cached_running_rsentries_.clear();
for (const Request& request : running_queue) {
for (const RequestStateEntry& rsentry : GetRequestState(request)->entries) {
// One request entry is considered as running for decode if it is a leaf and has
// finished all input prefill.
if (rsentry->status == RequestStateStatus::kAlive && rsentry->child_indices.empty() &&
rsentry->mstates[0]->inputs.empty()) {
cached_running_rsentries_.push_back(rsentry);
}
}
}
running_rsentries_changed = false;
}
return cached_running_rsentries_;
//
}
} // namespace serve
} // namespace llm
} // namespace mlc
+133
View File
@@ -0,0 +1,133 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/engine_state.h
*/
#ifndef MLC_LLM_SERVE_ENGINE_STATE_H_
#define MLC_LLM_SERVE_ENGINE_STATE_H_
#include <tvm/ffi/cast.h>
#include <tvm/ffi/string.h>
#include "config.h"
#include "metrics.h"
#include "prefix_cache.h"
#include "request.h"
#include "request_state.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
using tvm::ffi::GetRef;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
typedef TypedFunction<void(Array<RequestStreamOutput>)> FRequestStreamCallback;
/*! \brief The manager of internal id for requests in engine. */
struct EngineInternalIDManager {
std::vector<int64_t> available_ids;
int64_t id_cnt = 0;
/*! \brief Return an unused id. */
int64_t GetNewId() {
if (!available_ids.empty()) {
int64_t id = available_ids.back();
available_ids.pop_back();
return id;
} else {
return id_cnt++;
}
}
/*! \brief Recycle an id. */
void RecycleId(int64_t id) { available_ids.push_back(id); }
/*! \brief Reset the manager. */
void Reset() {
available_ids.clear();
id_cnt = 0;
}
};
/*! \brief The data structures used in the action post-process. */
struct ActionPostProcessWorkspace {
std::vector<RequestStateEntry> finished_rsentries;
Array<RequestStreamOutput> callback_delta_outputs;
};
/*!
* \brief The state of the running engine.
* It contains the requests and their states submitted to the Engine.
*/
class EngineStateObj : public Object {
public:
/*! \brief The requests being processed. */
std::vector<Request> running_queue;
/*! \brief The requests that have not started for process yet. */
std::vector<Request> waiting_queue;
/*! \brief The states of all requests. */
std::unordered_map<String, RequestState> request_states;
/*! \brief The internal id manager. */
EngineInternalIDManager id_manager;
/*! \brief Runtime metrics. */
EngineMetrics metrics;
/*! \brief The prefix cache. */
PrefixCache prefix_cache{nullptr};
/*! \brief A boolean flag denoting whether the running request state entry list has changed. */
bool running_rsentries_changed = true;
/*!
* \brief The current engine speculative decoding draft length.
* The length may change across time under the auto speculative decoding mode.
* Value 0 means undefined. It must have a positive value for speculative decoding to
* properly work.
*/
int spec_draft_length = 0;
/*! \brief A boolean flag denoting whether the engine is in disaggregation mode. */
bool disaggregation = false;
// Request stream callback function
FRequestStreamCallback request_stream_callback_;
/*!
* \brief The post-process data structures.
* We make it a workspace to avoid repetitive memory allocation/free in the action post process.
*/
ActionPostProcessWorkspace postproc_workspace;
/*! \brief Reset the engine state and clear the metrics. */
void Reset();
/*! \brief Get the request state of the given request. */
RequestState GetRequestState(Request request);
/*! \brief Return the running request state entries*/
const std::vector<RequestStateEntry>& GetRunningRequestStateEntries();
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<EngineStateObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("mlc.serve.EngineState", EngineStateObj, Object);
private:
std::vector<RequestStateEntry> cached_running_rsentries_;
};
/*!
* \brief Managed reference of EngineStateObj.
* \sa EngineStateObj
*/
class EngineState : public ObjectRef {
public:
explicit EngineState();
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(EngineState, ObjectRef, EngineStateObj);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_ENGINE_STATE_H_
+163
View File
@@ -0,0 +1,163 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/event_trace_recorder.cc
*/
#include "event_trace_recorder.h"
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
#include <algorithm>
#include <chrono>
#include <mutex>
#include <unordered_map>
#include <utility>
#include <vector>
namespace mlc {
namespace llm {
namespace serve {
using tvm::ffi::String;
namespace detail {
struct PairHash {
template <class T1, class T2>
std::size_t operator()(const std::pair<T1, T2>& p) const {
auto h1 = std::hash<T1>{}(p.first);
auto h2 = std::hash<T2>{}(p.second);
return h1 ^ h2;
}
};
} // namespace detail
/*! \brief The implementation of event trace recorder. */
class EventTraceRecorderImpl : public EventTraceRecorderObj {
public:
void AddEvent(const String& request_id, const std::string& event) final {
double event_time = std::chrono::duration_cast<std::chrono::duration<double>>(
std::chrono::system_clock::now().time_since_epoch())
.count();
{
std::lock_guard<std::mutex> lock(mutex_);
AddEventInternal(request_id, event, event_time);
}
}
void AddEvent(const Array<String>& request_ids, const std::string& event) final {
double event_time = std::chrono::duration_cast<std::chrono::duration<double>>(
std::chrono::system_clock::now().time_since_epoch())
.count(); // in seconds
{
std::lock_guard<std::mutex> lock(mutex_);
for (const String& request_id : request_ids) {
AddEventInternal(request_id, event, event_time);
}
}
}
std::string DumpJSON() final {
std::unordered_map<std::string, std::vector<std::pair<std::string, double>>> local_events;
{
std::lock_guard<std::mutex> lock(mutex_);
local_events = events_;
}
auto fcmp_events = [](const std::pair<int64_t, tvm::ffi::json::Value>& lhs,
const std::pair<int64_t, tvm::ffi::json::Value>& rhs) {
return lhs.first < rhs.first;
};
tvm::ffi::json::Array event_array;
for (const std::string& request_id : request_id_in_order_) {
std::vector<std::pair<std::string, double>> event_pairs = local_events.at(request_id);
std::vector<std::pair<int64_t, tvm::ffi::json::Value>> events_to_sort;
events_to_sort.reserve(event_pairs.size());
for (int i = 0; i < static_cast<int>(event_pairs.size()); ++i) {
std::string event = event_pairs[i].first;
double event_time = event_pairs[i].second;
std::string name;
std::string phase;
if (event.compare(0, 6, "start ") == 0) {
// Duration begin.
name = event.substr(6);
phase = "B";
} else if (event.compare(0, 7, "finish ") == 0) {
// Duration end.
name = event.substr(7);
phase = "E";
} else {
// Instant event.
name = event;
phase = "i";
}
int64_t event_time_in_us = static_cast<int64_t>(event_time * 1e6);
tvm::ffi::json::Object event_json;
event_json.Set("name", name);
event_json.Set("ph", phase);
event_json.Set("ts", event_time_in_us);
event_json.Set("pid", static_cast<int64_t>(1));
event_json.Set("tid", request_id);
events_to_sort.push_back({event_time_in_us, event_json});
}
std::sort(events_to_sort.begin(), events_to_sort.end(), fcmp_events);
for (auto [timestamp, event] : events_to_sort) {
event_array.push_back(std::move(event));
}
}
return tvm::ffi::json::Stringify(event_array);
}
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.EventTraceRecorder", EventTraceRecorderImpl,
EventTraceRecorderObj);
private:
/*! \brief The internal impl of AddEvent, taking the event time as input. */
void AddEventInternal(const std::string& request_id, const std::string& event,
double event_time) {
if (std::find(request_id_in_order_.begin(), request_id_in_order_.end(), request_id) ==
request_id_in_order_.end()) {
request_id_in_order_.push_back(request_id);
}
int event_cnt = event_counter_[{request_id, event}]++;
events_[request_id].push_back({event + " (" + std::to_string(event_cnt) + ")", event_time});
}
/*! \brief The mutex ensuring only one thread can access critical regions. */
std::mutex mutex_;
/************** Critical Regions **************/
/*! \brief The request ids in time order. Each id only appears once. */
std::vector<std::string> request_id_in_order_;
/*! \brief The number of a certain event for a request. */
std::unordered_map<std::pair<std::string, std::string>, int, detail::PairHash> event_counter_;
/*! \brief The event list of each request together with the timestamps. */
std::unordered_map<std::string, std::vector<std::pair<std::string, double>>> events_;
};
EventTraceRecorder EventTraceRecorder::Create() {
return EventTraceRecorder(tvm::ffi::make_object<EventTraceRecorderImpl>());
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
EventTraceRecorderImpl::RegisterReflection();
refl::GlobalDef()
.def("mlc.serve.EventTraceRecorder", []() { return EventTraceRecorder::Create(); })
.def("mlc.serve.EventTraceRecorderAddEvent",
[](const EventTraceRecorder& trace_recorder, const String& request_id,
const std::string& event) { trace_recorder->AddEvent(request_id, event); })
.def_method("mlc.serve.EventTraceRecorderDumpJSON", &EventTraceRecorderObj::DumpJSON);
}
} // namespace serve
} // namespace llm
} // namespace mlc
+81
View File
@@ -0,0 +1,81 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/event_trace_recorder.h
* \brief The event trace recorder for requests in MLC LLM.
*/
#ifndef MLC_LLM_SERVE_EVENT_TRACE_RECORDER_H_
#define MLC_LLM_SERVE_EVENT_TRACE_RECORDER_H_
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
#include <string>
namespace mlc {
namespace llm {
namespace serve {
using tvm::ffi::Array;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
using tvm::ffi::String;
/*! \brief The event trace recorder for requests. */
class EventTraceRecorderObj : public Object {
public:
/*!
* \brief Record a event for the input request in the trace recorder.
* \param request_id The subject request of the event.
* \param event The event in a string name.
* It can have one of the following patterns:
* - "start xxx", which marks the start of event "xxx",
* - "finish xxx", which marks the finish of event "xxx",
* - "yyy", which marks the instant event "yyy".
* The "starts" and "finishes" will be automatically paired in the trace recorder.
*/
virtual void AddEvent(const String& request_id, const std::string& event) = 0;
/*! \brief Record a event for the list of input requests. */
virtual void AddEvent(const Array<String>& request_ids, const std::string& event) = 0;
/*! \brief Dump the logged events in Chrome Trace Event Format in JSON string. */
virtual std::string DumpJSON() = 0;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<EventTraceRecorderObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.EventTraceRecorder", EventTraceRecorderObj, Object);
};
/*!
* \brief Managed reference to EventTraceRecorderObj.
* \sa EventTraceRecorderObj
*/
class EventTraceRecorder : public ObjectRef {
public:
/*! \brief Create an event trace recorder. */
static EventTraceRecorder Create();
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(EventTraceRecorder, ObjectRef, EventTraceRecorderObj);
};
/****************** Helper macro ******************/
/*! \brief Record a event for the input request or list or requests. */
#define RECORD_EVENT(trace_recorder, request_ids, event) \
if (trace_recorder.has_value()) { \
trace_recorder.value()->AddEvent(request_ids, event); \
}
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_EVENT_TRACE_RECORDER_H_
+373
View File
@@ -0,0 +1,373 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/function_table.cc
* \brief The implementation of function table in serving for distributed inference.
*/
#include "function_table.h"
#include <tvm/ffi/extra/module.h>
#include <tvm/ffi/function.h>
#include <tvm/runtime/disco/session.h>
#include <tvm/runtime/memory/memory_manager.h>
#include <tvm/runtime/tensor.h>
#include <cstdlib>
#include <filesystem>
#include <string>
#include <vector>
#include "../support/load_bytes_from_file.h"
#include "../support/utils.h"
#include "sampler/sampler.h"
namespace mlc {
namespace llm {
namespace serve {
Optional<Shape> GetDiscoWorkerCPUBinding(int num_workers) {
const char* raw_cpu_binding = std::getenv("MLC_DISCO_WORKER_CPU_BINDING");
if (raw_cpu_binding == nullptr) {
return std::nullopt;
}
std::string cpu_binding_str(raw_cpu_binding);
std::vector<std::string> cpu_ids_str = Split(cpu_binding_str, ',');
std::vector<int64_t> cpu_ids;
for (const std::string& cpu_id_str : cpu_ids_str) {
try {
cpu_ids.push_back(std::stol(cpu_id_str));
} catch (std::invalid_argument const& ex) {
LOG(FATAL) << "Invalid MLC_DISCO_WORKER_CPU_BINDING \"" << cpu_binding_str << "\"";
}
}
if (static_cast<int>(cpu_ids.size()) < num_workers) {
LOG(FATAL) << "Insufficient number of specified CPU workers in MLC_DISCO_WORKER_CPU_BINDING, "
"expecting at least "
<< num_workers << "CPU ids but only " << cpu_ids.size() << " are given.";
}
return Shape{cpu_ids};
}
Function FunctionTable::SessionFuncAsPackedFunc(Session sess, DRef sess_func, String name) {
return Function([sess, func = std::move(sess_func), name = std::move(name)](
ffi::PackedArgs args, ffi::Any* rv) -> void {
std::vector<AnyView> packed_args(args.size() + 3);
packed_args[0] = static_cast<int>(DiscoAction::kCallPacked);
packed_args[1] = 0;
packed_args[2] = func;
for (int i = 0; i < args.size(); ++i) {
packed_args[i + 3] = args[i];
}
*rv = sess->CallWithPacked(tvm::ffi::PackedArgs(packed_args.data(), packed_args.size()));
});
}
void FunctionTable::Init(String reload_lib_path, Device device, tvm::ffi::json::Object model_config,
Optional<Session> session, int num_shards, int num_stages) {
local_gpu_device = device;
this->model_config = model_config;
this->cached_buffers = Map<String, ObjectRef>();
int num_workers = num_shards * num_stages;
if (num_workers > 1) {
TVM_FFI_ICHECK(session.has_value());
this->sess = session.value();
this->use_disco = true;
this->disco_mod = sess->CallPacked(sess->GetGlobalFunc("runtime.disco.load_vm_module"),
reload_lib_path, Optional<Device>(std::nullopt));
this->mod_get_func = [this, fmodule_get_function = sess->GetGlobalFunc(
"ffi.ModuleGetFunction")](const std::string& name) -> Function {
DRef func = sess->CallPacked(fmodule_get_function, this->disco_mod, name, true);
bool exists = (func->DebugGetFromRemote(0).as<Function>()) != nullptr;
if (!exists) {
return Function(nullptr);
}
return SessionFuncAsPackedFunc(sess, func, name);
};
if (num_stages == 1) {
if (Optional<Shape> cpu_ids = GetDiscoWorkerCPUBinding(/*num_workers=*/num_shards)) {
Shape cpu_ids_value = cpu_ids.value();
sess->CallPacked(sess->GetGlobalFunc("runtime.disco.bind_worker_to_cpu_core"),
cpu_ids_value);
}
}
this->get_global_func = [this](const std::string& name) -> Function {
return SessionFuncAsPackedFunc(sess, sess->GetGlobalFunc(name), name);
};
this->model_metadata_ = ModelMetadata::FromModule(
this->disco_mod.value()->DebugGetFromRemote(0).cast<Module>(), std::move(model_config));
this->_InitFunctions();
} else {
TVM_FFI_ICHECK(!session.has_value());
Optional<Module> executable = std::nullopt;
Optional<Function> fload_exec;
if (StartsWith(reload_lib_path, "system://")) {
static Function f_load_system_lib = Function::GetGlobalRequired("ffi.SystemLib");
std::string system_lib_prefix = std::string(reload_lib_path).substr(9);
std::replace(system_lib_prefix.begin(), system_lib_prefix.end(), /*old=*/'-', /*new=*/'_');
executable = f_load_system_lib(system_lib_prefix + "_").cast<Module>();
fload_exec = executable.value()->GetFunction("vm_load_executable");
TVM_FFI_ICHECK(fload_exec.has_value())
<< "Cannot find system lib with " << system_lib_prefix
<< ", please make sure you set model_lib field consistently with the compilation ";
} else {
executable = tvm::ffi::Module::LoadFromFile(reload_lib_path);
fload_exec = executable.value()->GetFunction("vm_load_executable");
/* precompile opencl kernel programs */
if (device.device_type == kDLOpenCL) {
auto f_get = executable.value()->GetFunction("opencl.GetPreCompiledPrograms", true);
TVM_FFI_ICHECK(f_get.has_value()) << "Cannot find opencl.GetPreCompiledPrograms";
tvm::ffi::String bytes = f_get.value()().cast<String>();
auto f_set = executable.value()->GetFunction("opencl.SetPreCompiledPrograms", true);
TVM_FFI_ICHECK(f_set.has_value()) << "Cannot find opencl.SetPreCompiledPrograms";
f_set.value()(tvm::ffi::String(bytes));
}
TVM_FFI_ICHECK(fload_exec.has_value()) << "TVM runtime cannot find vm_load_executable";
}
this->use_disco = false;
this->local_vm = fload_exec.value()().cast<Module>();
this->local_vm.value()
->GetFunction("vm_initialization")
.value()(static_cast<int>(device.device_type), device.device_id,
static_cast<int>(tvm::runtime::memory::AllocatorType::kPooled),
static_cast<int>(kDLCPU), 0,
static_cast<int>(tvm::runtime::memory::AllocatorType::kPooled));
this->mod_get_func = [this](const std::string& name) -> Function {
return this->local_vm.value()->GetFunction(name, true).value_or(Function(nullptr));
};
this->get_global_func = [](const std::string& name) -> Function {
return Function::GetGlobalRequired(name);
};
this->model_metadata_ =
ModelMetadata::FromModule(this->local_vm.value(), std::move(model_config));
this->_InitFunctions();
}
TVM_FFI_ICHECK_EQ(this->model_metadata_.tensor_parallel_shards, num_shards);
TVM_FFI_ICHECK_EQ(this->model_metadata_.pipeline_parallel_stages, num_stages);
// Invoke the CUDA graph allocation init function if it is defined.
if (cuda_graph_alloc_init_func_.defined()) {
this->cuda_graph_alloc_init_func_();
}
}
ObjectRef FunctionTable::LoadParams(const std::string& model_path, Device device) {
if (this->use_disco) {
Optional<DRef> params = std::nullopt;
if (this->model_metadata_.params.empty()) {
std::filesystem::path fs_model_path = model_path;
std::string metadata_path = (fs_model_path / "tensor-cache.json").string();
std::string tensor_cache_metadata = LoadBytesFromFile(metadata_path);
Function loader_create = this->get_global_func("runtime.disco.ShardLoader");
auto load_all_func_name = "runtime.disco.ShardLoaderLoadAll";
Function loader_load_all = this->get_global_func(load_all_func_name);
TVM_FFI_ICHECK(loader_create != nullptr);
TVM_FFI_ICHECK(loader_load_all != nullptr);
DRef loader =
loader_create(metadata_path, tensor_cache_metadata, "", this->disco_mod).cast<DRef>();
params = loader_load_all(loader).cast<DRef>();
} else {
auto load_func_name = getenv("MLC_INTERNAL_PRESHARD_NUM") == nullptr
? "mlc.multi_gpu.LoadMultiGPU"
: "mlc.multi_gpu.LoadMultiGPUPresharded";
Function loader = this->get_global_func(load_func_name);
params = loader(model_path, this->disco_mod, tvm::ffi::json::Stringify(this->model_config))
.cast<DRef>();
}
return params.value();
} else {
static Function fload_cache = Function::GetGlobalRequired("vm.builtin.tensor_cache.load");
fload_cache(model_path, static_cast<int32_t>(device.device_type), device.device_id);
Array<Tensor> params;
if (this->model_metadata_.params.empty()) {
constexpr const char* name_loader = "vm.builtin.param_array_from_cache";
static Function fload_params = Function::GetGlobalRequired(name_loader);
params = fload_params("param", -1).cast<Array<Tensor>>();
} else {
constexpr const char* name_loader = "vm.builtin.param_array_from_cache_by_name";
static Function fload_params = Function::GetGlobalRequired(name_loader);
Array<String> param_names;
param_names.reserve(this->model_metadata_.params.size());
for (const auto& param : this->model_metadata_.params) {
param_names.push_back(param.name);
}
params = fload_params(param_names).cast<Array<Tensor>>();
}
// after we get params, it is safe to simply clear the cached version
// as these params are referenced by params_
static Function fclear_tensor_cache =
Function::GetGlobalRequired("vm.builtin.tensor_cache.clear");
fclear_tensor_cache();
return params;
}
}
void FunctionTable::_InitFunctions() {
this->embed_func_ = mod_get_func("embed");
this->image_embed_func_ = mod_get_func("image_embed");
this->single_batch_prefill_func_ = mod_get_func("prefill");
this->single_batch_decode_func_ = mod_get_func("decode");
this->single_batch_extend_func_ = mod_get_func("extend");
this->prefill_func_ = mod_get_func("batch_prefill");
this->decode_func_ = mod_get_func("batch_decode");
this->extend_func_ = mod_get_func("batch_extend");
this->verify_func_ = mod_get_func("batch_verify");
this->single_batch_prefill_to_last_hidden_func_ = mod_get_func("prefill_to_last_hidden_states");
this->single_batch_decode_to_last_hidden_func_ = mod_get_func("decode_to_last_hidden_states");
this->prefill_to_last_hidden_func_ = mod_get_func("batch_prefill_to_last_hidden_states");
this->decode_to_last_hidden_func_ = mod_get_func("batch_decode_to_last_hidden_states");
this->verify_to_last_hidden_func_ = mod_get_func("batch_verify_to_last_hidden_states");
this->fuse_embed_hidden_func_ = mod_get_func("fuse_embed_hidden_states");
Module mod = this->use_disco ? this->disco_mod.value()->DebugGetFromRemote(0).cast<Module>()
: this->local_vm.value();
this->get_logits_func_ = mod_get_func("get_logits");
this->batch_get_logits_func_ = mod_get_func("batch_get_logits");
this->batch_select_last_hidden_func_ = mod_get_func("batch_select_last_hidden_states");
this->softmax_func_ =
mod->GetFunction("softmax_with_temperature", true).value_or(Function(nullptr));
this->apply_logit_bias_func_ =
mod->GetFunction("apply_logit_bias_inplace", true).value_or(Function(nullptr));
this->apply_penalty_func_ =
mod->GetFunction("apply_penalty_inplace", true).value_or(Function(nullptr));
this->apply_bitmask_func_ =
mod->GetFunction("apply_bitmask_inplace", true).value_or(Function(nullptr));
this->alloc_embedding_tensor_func_ = mod_get_func("alloc_embedding_tensor");
this->cuda_graph_alloc_init_func_ = mod_get_func("cuda_graph_alloc_init");
this->create_kv_cache_func_ = mod_get_func("create_flashinfer_paged_kv_cache");
if (this->model_metadata_.sliding_window_size != -1 || !this->create_kv_cache_func_.defined()) {
Function f_create_rnn_state = mod_get_func("create_rnn_state");
if (this->model_metadata_.kv_state_kind == KVStateKind::kHybrid) {
// Hybrid models need both KV cache and RNN state.
this->create_kv_cache_func_ = mod_get_func("create_tir_paged_kv_cache");
this->create_rnn_state_func_ = f_create_rnn_state;
} else if (f_create_rnn_state.defined()) {
this->create_kv_cache_func_ = f_create_rnn_state;
} else {
this->create_kv_cache_func_ = mod_get_func("create_tir_paged_kv_cache");
}
}
this->reset_kv_cache_func_ = get_global_func("vm.builtin.kv_state_clear");
this->kv_cache_add_sequence_func_ = get_global_func("vm.builtin.kv_state_add_sequence");
this->kv_cache_fork_sequence_func_ = get_global_func("vm.builtin.kv_state_fork_sequence");
this->kv_cache_enable_sliding_window_for_seq_ =
get_global_func("vm.builtin.attention_kv_cache_enable_sliding_window_for_seq");
this->kv_cache_remove_sequence_func_ = get_global_func("vm.builtin.kv_state_remove_sequence");
this->kv_cache_begin_forward_func_ = get_global_func("vm.builtin.kv_state_begin_forward");
this->kv_cache_end_forward_func_ = get_global_func("vm.builtin.kv_state_end_forward");
this->kv_cache_disagg_prepare_recv_func_ =
get_global_func("vm.builtin.kv_cache_disagg_prepare_recv");
this->kv_cache_disagg_mark_send_func_ = get_global_func("vm.builtin.kv_cache_disagg_mark_send");
this->kv_cache_popn_func_ = get_global_func("vm.builtin.kv_state_popn");
this->kv_cache_commit_accepted_token_tree_nodes_func_ =
get_global_func("vm.builtin.attention_kv_cache_commit_accepted_token_tree_nodes");
this->kv_cache_get_num_available_pages_func_ =
Function::GetGlobalRequired("vm.builtin.attention_kv_cache_get_num_available_pages");
this->kv_cache_get_total_sequence_length_func_ =
Function::GetGlobalRequired("vm.builtin.attention_kv_cache_get_total_sequence_length");
if (Sampler::SupportGPUSampler(local_gpu_device)) {
gpu_multinomial_from_uniform_func_ =
mod->GetFunction("multinomial_from_uniform", true).value_or(Function(nullptr));
gpu_argsort_probs_func_ = mod->GetFunction("argsort_probs", true).value_or(Function(nullptr));
gpu_sample_with_top_p_func_ =
mod->GetFunction("sample_with_top_p", true).value_or(Function(nullptr));
gpu_sampler_take_probs_func_ =
mod->GetFunction("sampler_take_probs", true).value_or(Function(nullptr));
gpu_verify_draft_tokens_func_ =
mod->GetFunction("sampler_verify_draft_tokens", true).value_or(Function(nullptr));
gpu_renormalize_by_top_p_func_ =
mod->GetFunction("renormalize_by_top_p", true).value_or(Function(nullptr));
}
this->nd_view_func_ = get_global_func("vm.builtin.reshape");
this->nd_get_shape_func_ = get_global_func("vm.builtin.shape_of");
this->nd_copy_embedding_to_offset_func_ = get_global_func("mlc.copy_embedding_to_offset");
support_backtracking_kv_ = true;
this->tuple_getitem_func_ = get_global_func("vm.builtin.tuple_getitem");
if (use_disco) {
this->last_group_send_to_worker_0_ =
get_global_func("mlc.multi_gpu.SendFromLastGroupToWorker0");
}
this->gather_probs_func_ = mod->GetFunction("gather_probs", true).value_or(Function(nullptr));
this->scatter_probs_func_ = mod->GetFunction("scatter_probs", true).value_or(Function(nullptr));
this->gather_hidden_states_func_ = mod_get_func("gather_hidden_states");
this->scatter_hidden_states_func_ = mod_get_func("scatter_hidden_states");
}
ObjectRef FunctionTable::Empty(Shape shape, DLDataType dtype, Device device,
bool worker0_only) const {
if (this->use_disco) {
DRef empty_func = sess->GetGlobalFunc("runtime.disco.empty");
return sess->CallPacked(empty_func, shape, dtype, Optional<Device>(std::nullopt), worker0_only,
/*in_group=*/false);
} else {
return Tensor::Empty(shape, dtype, device);
}
}
ObjectRef FunctionTable::CopyToWorker0(const Tensor& host_array, String buffer_cache_key,
Shape max_reserved_shape, bool local_only) {
Map<String, ObjectRef> cached_buffers = this->cached_buffers.value();
if (this->use_disco && !local_only) {
Device null_device{DLDeviceType(0), 0};
Optional<DRef> buffer = std::nullopt;
auto it = cached_buffers.find(buffer_cache_key);
if (it != cached_buffers.end()) {
buffer = (*it).second.as_or_throw<DRef>();
} else {
buffer = this->Empty(max_reserved_shape, host_array.DataType(), null_device,
/*worker0_only=*/false)
.as_or_throw<DRef>();
cached_buffers.Set(buffer_cache_key, buffer.value());
}
Shape real_shape = host_array.Shape();
DRef buffer_view = nd_view_func_(buffer.value(), real_shape).cast<DRef>();
sess->CopyToWorker0(host_array, buffer_view);
return buffer_view;
} else {
auto it = cached_buffers.find(buffer_cache_key);
Tensor buffer{nullptr};
if (it != cached_buffers.end()) {
buffer = (*it).second.as_or_throw<Tensor>();
if (buffer_cache_key == "image") {
if (tvm::ffi::GetDataSize(*buffer.operator->()) <
tvm::ffi::GetDataSize(*host_array.operator->())) {
buffer = Tensor::Empty(max_reserved_shape, host_array->dtype, local_gpu_device);
cached_buffers.Set(buffer_cache_key, buffer);
}
}
} else {
buffer = Tensor::Empty(max_reserved_shape, host_array->dtype, local_gpu_device);
cached_buffers.Set(buffer_cache_key, buffer);
}
buffer = buffer.CreateView(host_array.Shape(), host_array->dtype);
DLTensor copy_dst = *(buffer.operator->());
Tensor::CopyFromTo(host_array.operator->(), &copy_dst);
return buffer;
}
}
void FunctionTable::DebugCallFuncOnAllAllWorker(const String& func_name,
Optional<String> func_args) const {
if (func_args) {
std::string args = func_args.value();
if (this->use_disco) {
sess->CallPacked(sess->GetGlobalFunc(func_name), args);
} else {
static Function func = Function::GetGlobalRequired(func_name);
func(args);
}
} else {
if (this->use_disco) {
sess->CallPacked(sess->GetGlobalFunc(func_name));
} else {
static Function func = Function::GetGlobalRequired(func_name);
func();
}
}
}
} // namespace serve
} // namespace llm
} // namespace mlc
+152
View File
@@ -0,0 +1,152 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/function_table.h
* \brief The header for function table in serving for distributed inference.
*/
#ifndef MLC_LLM_SERVE_FUNCTION_TABLE_H_
#define MLC_LLM_SERVE_FUNCTION_TABLE_H_
#include <tvm/ffi/container/map.h>
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/extra/module.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/optional.h>
#include <tvm/runtime/disco/session.h>
#include <tvm/runtime/tensor.h>
#include <string>
#include "../metadata/model.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::Device;
using namespace tvm::runtime;
using tvm::ffi::Function;
using tvm::ffi::Map;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
using tvm::ffi::Optional;
using tvm::ffi::Shape;
using tvm::ffi::TypedFunction;
//--------------------------------------------------------
// The function table under batching settings.
// The implementation is mostly the same as the one for
// single-sequence distributed inference in llm_chat.cc.
// The only difference is that the function table for
// batching uses a different set of packed functions.
//
// Here we choose to have the duplicate code instead of
// reusing the existing function table. This is mainly
// for the independent development of batching/serving
// and make the codebase manageable.
// We will eventually merge two implementation into one
// after the batching development becomes stable.
//--------------------------------------------------------
struct FunctionTable {
static Function SessionFuncAsPackedFunc(Session sess, DRef sess_func, String name);
void Init(String reload_lib_path, Device device, tvm::ffi::json::Object model_config,
Optional<Session> session, int num_shards, int num_stages);
ObjectRef LoadParams(const std::string& model_path, Device device);
void _InitFunctions();
ObjectRef Empty(Shape shape, DLDataType dtype, Device device, bool worker0_only) const;
/*!
* \brief Copy a host array to the worker or local gpu.
* \param host_array The host array to be copied.
* \param buffer_cache_key The key to the buffer cache.
* \param max_reserved_shape The maximum shape to be reserved in the buffer cache.
* \param local_only Whether to copy the array to the local gpu only. If true, the use_disco
* flag will be ignored. This can be useful for functions that run only on the
* local gpu when disco is enabled.
* \return The array on the worker or local gpu.
*/
ObjectRef CopyToWorker0(const Tensor& host_array, String buffer_cache_key,
Shape max_reserved_shape, bool local_only = false);
void DebugCallFuncOnAllAllWorker(const String& func_name, Optional<String> func_args) const;
bool use_disco = false;
Device local_gpu_device;
Session sess{nullptr};
Optional<DRef> disco_mod = std::nullopt;
Optional<Map<String, ObjectRef>> cached_buffers = std::nullopt;
Optional<tvm::ffi::Module> local_vm = std::nullopt;
tvm::ffi::json::Object model_config;
TypedFunction<Function(const std::string&)> mod_get_func;
TypedFunction<Function(const std::string&)> get_global_func;
ModelMetadata model_metadata_;
Function embed_func_;
Function image_embed_func_;
Function single_batch_prefill_func_;
Function single_batch_decode_func_;
Function single_batch_extend_func_;
Function prefill_func_;
Function decode_func_;
Function extend_func_;
Function verify_func_;
Function single_batch_prefill_to_last_hidden_func_;
Function single_batch_decode_to_last_hidden_func_;
Function prefill_to_last_hidden_func_;
Function decode_to_last_hidden_func_;
Function verify_to_last_hidden_func_;
Function fuse_embed_hidden_func_;
Function get_logits_func_;
Function batch_get_logits_func_;
Function batch_select_last_hidden_func_;
Function softmax_func_;
Function apply_logit_bias_func_;
Function apply_penalty_func_;
Function apply_bitmask_func_;
Function alloc_embedding_tensor_func_;
Function cuda_graph_alloc_init_func_;
Function create_kv_cache_func_;
Function create_rnn_state_func_;
Function reset_kv_cache_func_;
bool support_backtracking_kv_;
Function kv_cache_add_sequence_func_;
Function kv_cache_fork_sequence_func_;
Function kv_cache_enable_sliding_window_for_seq_;
Function kv_cache_remove_sequence_func_;
Function kv_cache_begin_forward_func_;
Function kv_cache_end_forward_func_;
Function kv_cache_disagg_prepare_recv_func_;
Function kv_cache_disagg_mark_send_func_;
Function kv_cache_popn_func_;
Function kv_cache_commit_accepted_token_tree_nodes_func_;
Function kv_cache_get_num_available_pages_func_;
Function kv_cache_get_total_sequence_length_func_;
Function gpu_multinomial_from_uniform_func_;
Function gpu_argsort_probs_func_;
Function gpu_sample_with_top_p_func_;
Function gpu_sampler_take_probs_func_;
Function gpu_verify_draft_tokens_func_;
Function gpu_renormalize_by_top_p_func_;
Function nd_view_func_;
Function nd_get_shape_func_;
Function nd_copy_embedding_to_offset_func_;
Function tuple_getitem_func_;
Function last_group_send_to_worker_0_;
// Auxiliary functions for speculative decoding.
Function gather_probs_func_;
Function scatter_probs_func_;
Function gather_hidden_states_func_;
Function scatter_hidden_states_func_;
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_FUNCTION_TABLE_H_
+509
View File
@@ -0,0 +1,509 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/logit_processor.cc
* \brief The implementation of logit processor.
*/
#include "logit_processor.h"
#include <tvm/ffi/function.h>
#include <tvm/runtime/device_api.h>
#include <tvm/support/cuda/nvtx.h>
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
inline void CopyArray(Tensor src, Tensor dst, TVMStreamHandle copy_stream) {
DLTensor dl_dst = *(dst.operator->());
Tensor::CopyFromTo(src.operator->(), &dl_dst, copy_stream);
}
inline void SyncCopyStream(Device device, TVMStreamHandle compute_stream,
TVMStreamHandle copy_stream) {
// - If there is no particular copy stream, no action is needed.
if (copy_stream == nullptr) {
return;
}
// - Sync two streams.
DeviceAPI::Get(device)->SyncStreamFromTo(device, copy_stream, compute_stream);
}
/***************** LogitProcessor Implementation *****************/
TVM_FFI_STATIC_INIT_BLOCK() { LogitProcessorObj::RegisterReflection(); }
class LogitProcessorImpl : public LogitProcessorObj {
public:
/*! * \brief Constructor of LogitProcessorImpl. */
explicit LogitProcessorImpl(int max_num_token, int vocab_size, FunctionTable* ft, DLDevice device,
Optional<EventTraceRecorder> trace_recorder)
: max_num_token_(max_num_token),
vocab_size_(vocab_size),
bitmask_size_((vocab_size + 31) / 32),
softmax_func_(ft->softmax_func_),
device_(device),
apply_logit_bias_func_(ft->apply_logit_bias_func_),
apply_penalty_func_(ft->apply_penalty_func_),
apply_bitmask_func_(ft->apply_bitmask_func_),
trace_recorder_(std::move(trace_recorder)) {
Device preferred_host_device = GetPreferredHostDevice(device);
// Initialize auxiliary arrays on CPU.
seq_ids_host_ = Tensor::Empty({max_num_token}, dtype_i32_, preferred_host_device);
pos2seq_id_host_ =
Tensor::Empty({max_num_token * vocab_size}, dtype_i32_, preferred_host_device);
token_ids_host_ =
Tensor::Empty({max_num_token * vocab_size}, dtype_i32_, preferred_host_device);
token_cnt_host_ =
Tensor::Empty({max_num_token * vocab_size}, dtype_i32_, preferred_host_device);
token_logit_bias_host_ =
Tensor::Empty({max_num_token * vocab_size}, dtype_f32_, preferred_host_device);
penalties_host_ = Tensor::Empty({max_num_token, 3}, dtype_f32_, preferred_host_device);
bitmask_host_ =
Tensor::Empty({max_num_token, bitmask_size_}, dtype_i32_, preferred_host_device);
temperature_host_ = Tensor::Empty({max_num_token}, dtype_f32_, preferred_host_device);
// Initialize auxiliary arrays on GPU.
seq_ids_device_ = Tensor::Empty({max_num_token}, dtype_i32_, device);
pos2seq_id_device_ = Tensor::Empty({max_num_token * vocab_size}, dtype_i32_, device);
token_ids_device_ = Tensor::Empty({max_num_token * vocab_size}, dtype_i32_, device);
token_cnt_device_ = Tensor::Empty({max_num_token * vocab_size}, dtype_i32_, device);
token_logit_bias_device_ = Tensor::Empty({max_num_token * vocab_size}, dtype_f32_, device);
penalties_device_ = Tensor::Empty({max_num_token, 3}, dtype_f32_, device);
bitmask_device_ = Tensor::Empty({max_num_token, bitmask_size_}, dtype_i32_, device);
temperature_device_ = Tensor::Empty({max_num_token}, dtype_f32_, device);
TVM_FFI_ICHECK(apply_logit_bias_func_.defined())
<< "Function \"apply_logit_bias_inplace\" not found in model";
TVM_FFI_ICHECK(apply_penalty_func_.defined())
<< "Function \"apply_penalty_inplace\" not found in model";
TVM_FFI_ICHECK(apply_bitmask_func_.defined())
<< "Function \"apply_bitmask_inplace\" not found in model";
// If the device is CUDA/ROCm, we create a standalone copy stream, in
// purpose to hide the latency of auxiliary stream copy.
if (device.device_type == DLDeviceType::kDLCUDA ||
device.device_type == DLDeviceType::kDLROCM) {
// The compute stream is the default stream.
compute_stream_ = DeviceAPI::Get(device)->GetCurrentStream(device);
copy_stream_ = DeviceAPI::Get(device)->CreateStream(device);
}
}
~LogitProcessorImpl() {
// Free the copy stream if defined.
if (copy_stream_ != nullptr) {
DeviceAPI::Get(device_)->FreeStream(device_, copy_stream_);
}
}
void InplaceUpdateLogits(Tensor logits, //
const Array<GenerationConfig>& generation_cfg, //
const Array<RequestModelState>& mstates, //
const Array<String>& request_ids, //
const std::vector<int>* cum_num_token, //
const Array<RequestModelState>* draft_mstates, //
const std::vector<std::vector<int>>* draft_token_indices) final {
NVTXScopedRange nvtx_scope("Logit inplace update");
TVM_FFI_ICHECK_EQ(logits->ndim, 2);
TVM_FFI_ICHECK_EQ(logits->shape[1], vocab_size_);
TVM_FFI_ICHECK(logits.DataType() == (DLDataType{kDLFloat, 32, 1}));
TVM_FFI_ICHECK_EQ(generation_cfg.size(), mstates.size());
TVM_FFI_ICHECK_LE(logits->shape[0], max_num_token_);
int num_total_token = logits->shape[0];
int num_sequence = generation_cfg.size();
TVM_FFI_ICHECK((draft_mstates == nullptr) == (draft_token_indices == nullptr));
if (cum_num_token != nullptr) {
TVM_FFI_ICHECK(draft_mstates != nullptr);
TVM_FFI_ICHECK_EQ(cum_num_token->size(), num_sequence + 1);
TVM_FFI_ICHECK_EQ(cum_num_token->back(), num_total_token);
} else {
TVM_FFI_ICHECK_EQ(num_sequence, num_total_token);
}
if (draft_mstates != nullptr) {
TVM_FFI_ICHECK_EQ(draft_mstates->size(), num_sequence);
TVM_FFI_ICHECK_EQ(draft_token_indices->size(), num_sequence);
}
RECORD_EVENT(trace_recorder_, request_ids, "start update logits");
// Update 1. logit bias
RECORD_EVENT(trace_recorder_, request_ids, "start apply logit bias");
UpdateWithLogitBias(logits, generation_cfg, cum_num_token);
RECORD_EVENT(trace_recorder_, request_ids, "finish apply logit bias");
// Update 2. penalties
RECORD_EVENT(trace_recorder_, request_ids, "start apply penalty");
UpdateWithPenalty(logits, generation_cfg, mstates, cum_num_token, draft_mstates,
draft_token_indices);
RECORD_EVENT(trace_recorder_, request_ids, "finish apply penalty");
// Update 3. Vocabulary mask.
// Note: The mask application must be placed as the last step in logit processor.
// This is because the masked logits are set to the minimal value.
// Further logit subtraction may cause issue such as underflow.
RECORD_EVENT(trace_recorder_, request_ids, "start apply logit mask");
UpdateWithMask(logits, mstates, cum_num_token, draft_mstates, draft_token_indices);
RECORD_EVENT(trace_recorder_, request_ids, "finish apply logit mask");
RECORD_EVENT(trace_recorder_, request_ids, "finish update logits");
}
Tensor ComputeProbsFromLogits(Tensor logits, const Array<GenerationConfig>& generation_cfg,
const Array<String>& request_ids,
const std::vector<int>* cum_num_token) final {
NVTXScopedRange nvtx_scope("Compute probs from logits");
// logits: (n, v)
TVM_FFI_ICHECK_EQ(logits->ndim, 2);
TVM_FFI_ICHECK_LE(logits->shape[0], max_num_token_);
TVM_FFI_ICHECK_EQ(logits->shape[1], vocab_size_);
TVM_FFI_ICHECK(logits.DataType() == (DLDataType{kDLFloat, 32, 1}));
int num_total_token = logits->shape[0];
int num_sequence = generation_cfg.size();
if (cum_num_token != nullptr) {
TVM_FFI_ICHECK_EQ(cum_num_token->size(), num_sequence + 1);
TVM_FFI_ICHECK_EQ(cum_num_token->back(), num_total_token);
} else {
TVM_FFI_ICHECK_EQ(num_sequence, num_total_token);
}
RECORD_EVENT(trace_recorder_, request_ids, "start softmax");
// Construct:
// - temperature (max_num_token,) float32
float* p_temperature = static_cast<float*>(temperature_host_->data);
// - Set arrays.
for (int i = 0; i < num_sequence; ++i) {
int num_token_to_process =
cum_num_token == nullptr ? 1 : (cum_num_token->at(i + 1) - cum_num_token->at(i));
int token_offset = cum_num_token == nullptr ? i : cum_num_token->at(i);
for (int j = 0; j < num_token_to_process; ++j) {
p_temperature[token_offset + j] = std::max(generation_cfg[i]->temperature, 0.0);
}
}
// - View arrays.
Tensor temperature_host = temperature_host_.CreateView({num_total_token}, dtype_f32_);
Tensor temperature_device = temperature_device_.CreateView({num_total_token}, dtype_f32_);
// - Copy arrays to GPU.
CopyArray(/*src=*/temperature_host, /*dst=*/temperature_device, copy_stream_);
SyncCopyStream(device_, compute_stream_, copy_stream_);
// - Call kernel.
Tensor probs = softmax_func_(logits.CreateView({num_total_token, 1, vocab_size_}, dtype_f32_),
temperature_device)
.cast<Tensor>();
TVM_FFI_ICHECK_EQ(probs->ndim, 3);
TVM_FFI_ICHECK_EQ(probs->shape[0], num_total_token);
TVM_FFI_ICHECK_EQ(probs->shape[1], 1);
TVM_FFI_ICHECK_EQ(probs->shape[2], vocab_size_);
if (trace_recorder_.has_value()) {
DeviceAPI::Get(device_)->StreamSync(device_, /*stream=*/nullptr);
}
RECORD_EVENT(trace_recorder_, request_ids, "finish softmax");
return probs.CreateView({num_total_token, vocab_size_}, probs->dtype);
}
private:
void UpdateWithLogitBias(Tensor logits, const Array<GenerationConfig>& generation_cfg,
const std::vector<int>* cum_num_token) {
NVTXScopedRange nvtx_scope("UpdateWithLogitBias");
// Construct:
// - pos2seq_id (max_num_token * vocab_size,) int32
// - token_ids (max_num_token * vocab_size,) int32
// - token_logit_bias (max_num_token * vocab_size,) float32
int* p_pos2seq_id = static_cast<int*>(pos2seq_id_host_->data);
int* p_token_ids = static_cast<int*>(token_ids_host_->data);
float* p_token_logit_bias = static_cast<float*>(token_logit_bias_host_->data);
// - Set arrays.
int num_token_for_bias = 0;
int num_bias_token = 0;
for (int i = 0; i < static_cast<int>(generation_cfg.size()); ++i) {
int num_token_to_process =
cum_num_token == nullptr ? 1 : (cum_num_token->at(i + 1) - cum_num_token->at(i));
int token_offset = cum_num_token == nullptr ? i : cum_num_token->at(i);
for (int j = 0; j < num_token_to_process; ++j) {
if (!generation_cfg[i]->logit_bias.empty()) {
for (auto [token_id, bias] : generation_cfg[i]->logit_bias) {
p_pos2seq_id[num_bias_token] = token_offset + j;
p_token_ids[num_bias_token] = token_id;
p_token_logit_bias[num_bias_token] = bias;
++num_bias_token;
}
++num_token_for_bias;
}
}
}
if (num_token_for_bias == 0) {
return;
}
// - View arrays.
int num_token = num_bias_token;
Tensor pos2seq_id_host = pos2seq_id_host_.CreateView({num_token}, dtype_i32_);
Tensor pos2seq_id_device = pos2seq_id_device_.CreateView({num_token}, dtype_i32_);
Tensor token_ids_host = token_ids_host_.CreateView({num_token}, dtype_i32_);
Tensor token_ids_device = token_ids_device_.CreateView({num_token}, dtype_i32_);
Tensor token_logit_bias_host = token_logit_bias_host_.CreateView({num_token}, dtype_f32_);
Tensor token_logit_bias_device = token_logit_bias_device_.CreateView({num_token}, dtype_f32_);
// - Copy arrays to GPU.
CopyArray(/*src=*/pos2seq_id_host, /*dst=*/pos2seq_id_device, copy_stream_);
CopyArray(/*src=*/token_ids_host, /*dst=*/token_ids_device, copy_stream_);
CopyArray(/*src=*/token_logit_bias_host, /*dst=*/token_logit_bias_device, copy_stream_);
SyncCopyStream(device_, compute_stream_, copy_stream_);
// - Call kernel.
apply_logit_bias_func_(logits, pos2seq_id_device, token_ids_device, token_logit_bias_device);
if (trace_recorder_.has_value()) {
DeviceAPI::Get(device_)->StreamSync(device_, nullptr);
}
}
void UpdateWithPenalty(Tensor logits, const Array<GenerationConfig>& generation_cfg,
const Array<RequestModelState>& mstates,
const std::vector<int>* cum_num_token,
const Array<RequestModelState>* draft_mstates,
const std::vector<std::vector<int>>* draft_token_indices) {
NVTXScopedRange nvtx_scope("UpdateWithPenalty");
// Construct:
// - seq_ids (max_num_token,) int32
// - pos2seq_id (max_num_token * vocab_size,) int32
// - token_ids (max_num_token * vocab_size,) int32
// - token_cnt (max_num_token * vocab_size,) int32
// - penalties (max_num_token, 3) float32
int* p_seq_ids = static_cast<int*>(seq_ids_host_->data);
int* p_pos2seq_id = static_cast<int*>(pos2seq_id_host_->data);
int* p_token_ids = static_cast<int*>(token_ids_host_->data);
int* p_token_cnt = static_cast<int*>(token_cnt_host_->data);
float* p_penalties = static_cast<float*>(penalties_host_->data);
// - Set arrays.
int num_token_for_penalty = 0;
int num_penalty_appeared_token = 0;
for (int i = 0; i < static_cast<int>(generation_cfg.size()); ++i) {
if (generation_cfg[i]->frequency_penalty != 0.0 ||
generation_cfg[i]->presence_penalty != 0.0 ||
generation_cfg[i]->repetition_penalty != 1.0) {
int num_token_to_process =
cum_num_token == nullptr ? 1 : (cum_num_token->at(i + 1) - cum_num_token->at(i));
int token_offset = cum_num_token == nullptr ? i : cum_num_token->at(i);
TVM_FFI_ICHECK(num_token_to_process == 1 || mstates[i]->draft_output_tokens.empty());
TVM_FFI_ICHECK(draft_token_indices == nullptr ||
draft_token_indices->at(i).size() == num_token_to_process);
for (int j = 0; j < num_token_to_process; ++j) {
p_seq_ids[num_token_for_penalty] = token_offset + j;
std::vector<SampleResult> draft_token_seq;
// Update appeared_token_ids with draft tokens
if (draft_token_indices != nullptr) {
int cur_draft_token_index = draft_token_indices->at(i)[j];
while (cur_draft_token_index != -1) {
draft_token_seq.push_back(
(*draft_mstates)[i]->draft_output_tokens[cur_draft_token_index]);
cur_draft_token_index =
(*draft_mstates)[i]->draft_token_parent_idx[cur_draft_token_index];
}
}
auto& appeared_token_ids = mstates[i]->appeared_token_ids;
for (const auto& token : draft_token_seq) {
appeared_token_ids[token.GetTokenId()] += 1;
}
for (auto [token_id, cnt] : appeared_token_ids) {
p_pos2seq_id[num_penalty_appeared_token] = num_token_for_penalty;
p_token_ids[num_penalty_appeared_token] = token_id;
p_token_cnt[num_penalty_appeared_token] = cnt;
++num_penalty_appeared_token;
}
for (const auto& token : draft_token_seq) {
if ((--appeared_token_ids[token.GetTokenId()]) == 0) {
appeared_token_ids.erase(token.GetTokenId());
}
}
p_penalties[num_token_for_penalty * 3] = generation_cfg[i]->presence_penalty;
p_penalties[num_token_for_penalty * 3 + 1] = generation_cfg[i]->frequency_penalty;
p_penalties[num_token_for_penalty * 3 + 2] = generation_cfg[i]->repetition_penalty;
++num_token_for_penalty;
}
}
}
if (num_token_for_penalty == 0) {
return;
}
// - View arrays.
int num_seq = num_token_for_penalty;
int num_token = num_penalty_appeared_token;
Tensor seq_ids_host = seq_ids_host_.CreateView({num_seq}, dtype_i32_);
Tensor seq_ids_device = seq_ids_device_.CreateView({num_seq}, dtype_i32_);
Tensor pos2seq_id_host = pos2seq_id_host_.CreateView({num_token}, dtype_i32_);
Tensor pos2seq_id_device = pos2seq_id_device_.CreateView({num_token}, dtype_i32_);
Tensor token_ids_host = token_ids_host_.CreateView({num_token}, dtype_i32_);
Tensor token_ids_device = token_ids_device_.CreateView({num_token}, dtype_i32_);
Tensor token_cnt_host = token_cnt_host_.CreateView({num_token}, dtype_i32_);
Tensor token_cnt_device = token_cnt_device_.CreateView({num_token}, dtype_i32_);
Tensor penalties_host = penalties_host_.CreateView({num_seq, 3}, dtype_f32_);
Tensor penalties_device = penalties_device_.CreateView({num_seq, 3}, dtype_f32_);
// - Copy arrays to GPU.
CopyArray(/*src=*/seq_ids_host, /*dst=*/seq_ids_device, copy_stream_);
CopyArray(/*src=*/pos2seq_id_host, /*dst=*/pos2seq_id_device, copy_stream_);
CopyArray(/*src=*/token_ids_host, /*dst=*/token_ids_device, copy_stream_);
CopyArray(/*src=*/token_cnt_host, /*dst=*/token_cnt_device, copy_stream_);
CopyArray(/*src=*/penalties_host, /*dst=*/penalties_device, copy_stream_);
SyncCopyStream(device_, compute_stream_, copy_stream_);
// - Call kernel.
apply_penalty_func_(logits, seq_ids_device, pos2seq_id_device, token_ids_device,
token_cnt_device, penalties_device);
if (trace_recorder_.has_value()) {
DeviceAPI::Get(device_)->StreamSync(device_, nullptr);
}
}
void UpdateWithMask(Tensor logits, const Array<RequestModelState>& mstates,
const std::vector<int>* cum_num_token,
const Array<RequestModelState>* draft_mstates,
const std::vector<std::vector<int>>* draft_token_indices) {
NVTXScopedRange nvtx_scope("UpdateWithMask");
// Construct:
// - seq_ids (max_num_token,) int32
// - bitmask (max_num_token, ceildiv(vocab_size, 32)), int32
int32_t* p_seq_ids = static_cast<int32_t*>(seq_ids_host_->data);
uint32_t* p_bitmask = static_cast<uint32_t*>(bitmask_host_->data);
// - Set arrays.
int batch_size = logits->shape[0];
TVM_FFI_ICHECK((cum_num_token == nullptr && batch_size == mstates.size()) ||
(cum_num_token != nullptr && batch_size == cum_num_token->back()));
std::memset(p_seq_ids, 0, batch_size * sizeof(int32_t));
for (int i = 0; i < static_cast<int>(mstates.size()); ++i) {
int token_start_offset = cum_num_token == nullptr ? i : cum_num_token->at(i);
int token_number =
cum_num_token == nullptr ? 1 : (cum_num_token->at(i + 1) - cum_num_token->at(i));
bool require_mask = mstates[i]->RequireNextTokenBitmask();
TVM_FFI_ICHECK(draft_token_indices == nullptr ||
draft_token_indices->at(i).size() == token_number);
for (int j = 0; j < token_number; ++j) {
if (require_mask) {
std::vector<SampleResult> draft_token_seq;
if (draft_token_indices != nullptr) {
int cur_draft_token_index = draft_token_indices->at(i)[j];
while (cur_draft_token_index != -1) {
draft_token_seq.push_back(
(*draft_mstates)[i]->draft_output_tokens[cur_draft_token_index]);
cur_draft_token_index =
(*draft_mstates)[i]->draft_token_parent_idx[cur_draft_token_index];
}
for (auto it = draft_token_seq.rbegin(); it != draft_token_seq.rend(); ++it) {
mstates[i]->grammar_matcher.value().AcceptToken(it->GetTokenId());
}
}
// Find a slice of bitmask_host_: bitmask_host_[num_token_for_mask, :]
DLTensor bitmask_dltensor = *bitmask_host_.operator->();
int64_t bitmask_shape[] = {bitmask_size_};
bitmask_dltensor.data = p_bitmask + (token_start_offset + j) * bitmask_size_;
bitmask_dltensor.shape = bitmask_shape;
bitmask_dltensor.ndim = 1;
mstates[i]->GetNextTokenBitmask(&bitmask_dltensor);
p_seq_ids[token_start_offset + j] = 1;
if (draft_token_seq.size() > 0) {
mstates[i]->grammar_matcher.value().Rollback(draft_token_seq.size());
}
}
}
}
int num_token_for_mask = 0;
for (int i = 0; i < batch_size; ++i) {
if (p_seq_ids[i] == 1) {
p_seq_ids[num_token_for_mask] = i;
++num_token_for_mask;
}
}
if (num_token_for_mask == 0) {
return;
}
// - View arrays.
int num_seq = num_token_for_mask;
Tensor seq_ids_host = seq_ids_host_.CreateView({num_seq}, dtype_i32_);
Tensor seq_ids_device = seq_ids_device_.CreateView({num_seq}, dtype_i32_);
Tensor bitmask_host = bitmask_host_.CreateView({batch_size, bitmask_size_}, dtype_i32_);
Tensor bitmask_device = bitmask_device_.CreateView({batch_size, bitmask_size_}, dtype_i32_);
// - Copy arrays to GPU.
CopyArray(/*src=*/seq_ids_host, /*dst=*/seq_ids_device, copy_stream_);
CopyArray(/*src=*/bitmask_host, /*dst=*/bitmask_device, copy_stream_);
SyncCopyStream(device_, compute_stream_, copy_stream_);
// - Call kernel.
apply_bitmask_func_(logits, seq_ids_device, bitmask_device);
if (trace_recorder_.has_value()) {
DeviceAPI::Get(device_)->StreamSync(device_, nullptr);
}
}
// Model configurations
const int max_num_token_;
const int vocab_size_;
const int bitmask_size_;
const DLDataType dtype_i32_ = DLDataType{kDLInt, 32, 1};
const DLDataType dtype_u32_ = DLDataType{kDLUInt, 32, 1};
const DLDataType dtype_f32_ = DLDataType{kDLFloat, 32, 1};
// Packed functions.
Device device_;
Function softmax_func_;
Function apply_logit_bias_func_;
Function apply_penalty_func_;
Function apply_bitmask_func_;
// Auxiliary Tensors on CPU
Tensor seq_ids_host_;
Tensor pos2seq_id_host_;
Tensor token_ids_host_;
Tensor token_cnt_host_;
Tensor token_logit_bias_host_;
Tensor penalties_host_;
Tensor bitmask_host_;
Tensor temperature_host_;
// Auxiliary Tensors on GPU
Tensor seq_ids_device_;
Tensor pos2seq_id_device_;
Tensor token_ids_device_;
Tensor token_cnt_device_;
Tensor token_logit_bias_device_;
Tensor penalties_device_;
Tensor bitmask_device_;
Tensor temperature_device_;
// Event trace recorder.
Optional<EventTraceRecorder> trace_recorder_;
// The device stream for the default computation operations.
TVMStreamHandle compute_stream_ = nullptr;
// The device stream for copying auxiliary data structure to GPU.
TVMStreamHandle copy_stream_ = nullptr;
// A small epsilon.
const double eps_ = 1e-5;
};
LogitProcessor::LogitProcessor(int max_num_token, int vocab_size, FunctionTable* ft,
DLDevice device, Optional<EventTraceRecorder> trace_recorder) {
data_ = tvm::ffi::make_object<LogitProcessorImpl>(max_num_token, vocab_size, ft, device,
std::move(trace_recorder));
}
} // namespace serve
} // namespace llm
} // namespace mlc
+104
View File
@@ -0,0 +1,104 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/logit_processor.h
* \brief The header for logit processor.
*/
#ifndef MLC_LLM_SERVE_LOGIT_PROCESSOR_H_
#define MLC_LLM_SERVE_LOGIT_PROCESSOR_H_
#include <tvm/ffi/extra/module.h>
#include <tvm/ffi/string.h>
#include "../base.h"
#include "config.h"
#include "event_trace_recorder.h"
#include "function_table.h"
#include "request_state.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::Device;
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
/*!
* \brief The logit processor class that updates logits with regard
* presence/frequency penalties, logit bias, etc..
*/
class LogitProcessorObj : public Object {
public:
/*!
* \brief In-place update a batch of logits with regard to the given
* generation config and request states.
* \param logits The batch of raw logits, in shape (num_total_token, vocab_size),
* where `num_total_token` may be larger than the number of sequences
* indicated by `generation_cfg`, in which case some sequences may have
* more than one token.
* \param generation_cfg The generation config of each sequence in the batch.
* \param mstates The request states of each sequence in the batch.
* \param request_ids The ids of each request.
* \param cum_num_token The pointer to the cumulative token length of the sequences.
* If the pointer is nullptr, it means each sequence has only one token.
* \param draft_mstates Optional. The draft request states of each sequence.
* \param draft_token_indices Optional. The pointer to the draft token indices of each draft token
* in the model state (-1 indicates the token is not a draft token) when speculation is enabled.
* This is used to compute the sequence state with the draft tokens considered (the saved sequence
* state is not updated with the draft tokens).
*/
virtual void InplaceUpdateLogits(
Tensor logits, const Array<GenerationConfig>& generation_cfg,
const Array<RequestModelState>& mstates, const Array<String>& request_ids,
const std::vector<int>* cum_num_token = nullptr,
const Array<RequestModelState>* draft_mstates = nullptr,
const std::vector<std::vector<int>>* draft_token_indices = nullptr) = 0;
/*!
* \brief Compute probability distributions for the input batch of logits.
* \param logits The batch of updated logits.
* \param generation_cfg The generation config of each sequence in the batch.
* \param request_ids The ids of each request.
* \param cum_num_token The pointer to the cumulative token length of the sequences.
* If the pointer is nullptr, it means each sequence has only one token.
* \return The batch of computed probability distributions on GPU.
*/
virtual Tensor ComputeProbsFromLogits(Tensor logits,
const Array<GenerationConfig>& generation_cfg,
const Array<String>& request_ids,
const std::vector<int>* cum_num_token = nullptr) = 0;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<LogitProcessorObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.LogitProcessor", LogitProcessorObj, Object);
};
class LogitProcessor : public ObjectRef {
public:
/*!
* \brief Constructor.
* \param max_num_token The max number of tokens in the token processor.
* \param vocab_size The model's vocabulary size.
* \param ft The packed function table.
* \param device The device that the model runs on.
* \param trace_recorder The event trace recorder.
*/
explicit LogitProcessor(int max_num_token, int vocab_size, FunctionTable* ft, DLDevice device,
Optional<EventTraceRecorder> trace_recorder);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(LogitProcessor, ObjectRef, LogitProcessorObj);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_LOGIT_PROCESSOR_H_
+185
View File
@@ -0,0 +1,185 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/metrics.cc
*/
#include "metrics.h"
#include <tvm/runtime/logging.h>
#include <sstream>
namespace mlc {
namespace llm {
namespace serve {
tvm::ffi::json::Object TimeCost::AsJSON() const {
tvm::ffi::json::Object config;
config.Set("count", count);
if (count != 0) {
config.Set("mean", sum / count);
}
return config;
}
tvm::ffi::json::Object SpecDecodeMetrics::AsJSON() const {
tvm::ffi::json::Object metrics;
auto f_vector_to_array = [](const std::vector<int64_t>& vec) {
tvm::ffi::json::Array arr;
for (int64_t v : vec) {
arr.push_back(v);
}
return tvm::ffi::json::Value(arr);
};
metrics.Set("draft_count", f_vector_to_array(draft_count));
metrics.Set("accept_count", f_vector_to_array(accept_count));
TVM_FFI_ICHECK_EQ(draft_count.size(), accept_count.size());
// NOTE: label follows prometheus with full context
// so it can be flattened and used in metrics reoorting end point
tvm::ffi::json::Object accept_prob_metrics;
tvm::ffi::json::Object accept_rate_metrics;
tvm::ffi::json::Object accept_len_metrics;
double accept_len_value = 0;
for (size_t i = 0; i < draft_count.size(); ++i) {
std::ostringstream accept_prob_label;
accept_prob_label << "accept_prob{step=" << i << "}";
double accept_prob_value =
(static_cast<double>(accept_count[i]) / static_cast<double>(draft_count[i]));
accept_prob_metrics.Set(accept_prob_label.str(), accept_prob_value);
accept_len_value += accept_prob_value;
std::ostringstream accept_len_label;
accept_len_label << "accept_len{step=" << i << "}";
accept_len_metrics.Set(accept_len_label.str(), accept_len_value);
if (i != 0) {
std::ostringstream accept_rate_label;
accept_rate_label << "accept_rate{step=" << i << "}";
double accept_rate_value =
accept_count[i - 1] == 0
? 0.0f
: (static_cast<double>(accept_count[i]) / static_cast<double>(accept_count[i - 1]));
accept_rate_metrics.Set(accept_rate_label.str(), accept_rate_value);
}
}
metrics.Set("accept_prob", accept_prob_metrics);
metrics.Set("accept_rate", accept_rate_metrics);
metrics.Set("accept_len", accept_len_metrics);
return metrics;
}
tvm::ffi::json::Object RequestMetrics::AsJSON() const {
tvm::ffi::json::Object metrics;
metrics.Set("prompt_tokens", prompt_tokens);
metrics.Set("completion_tokens", completion_tokens);
metrics.Set("prefill_tokens", prefill_tokens);
metrics.Set("decode_tokens", decode_tokens);
metrics.Set("jump_forward_tokens", jump_forward_tokens);
if (prefill_tokens != 0) {
metrics.Set("prefill_tokens_per_s", prefill_tokens / this->GetPrefillTime());
}
if (decode_tokens != 0) {
metrics.Set("decode_tokens_per_s", decode_tokens / this->GetDecodeTime());
}
metrics.Set("end_to_end_latency_s", this->GetTotalTime());
metrics.Set("ttft_s", this->GetTTFT());
metrics.Set("inter_token_latency_s", this->GetInterTokenLatency());
return metrics;
}
std::string RequestMetrics::AsUsageJSONStr(bool include_extra) const {
tvm::ffi::json::Object usage;
usage.Set("prompt_tokens", prompt_tokens);
usage.Set("completion_tokens", completion_tokens);
usage.Set("total_tokens", prompt_tokens + completion_tokens);
if (include_extra) {
usage.Set("extra", this->AsJSON());
}
return tvm::ffi::json::Stringify(usage);
}
tvm::ffi::json::Object EngineMetrics::AsJSON() const {
tvm::ffi::json::Object metrics;
metrics.Set("engine_prefill_time_sum", engine_prefill_time_sum);
metrics.Set("engine_decode_time_sum", engine_decode_time_sum);
metrics.Set("engine_jump_forward_time_sum", engine_jump_forward_time_sum);
metrics.Set("prompt_tokens_sum", prompt_tokens_sum);
metrics.Set("completion_tokens_sum", completion_tokens_sum);
metrics.Set("prefill_tokens_sum", prefill_tokens_sum);
metrics.Set("decode_tokens_sum", decode_tokens_sum);
metrics.Set("jump_forward_tokens_sum", jump_forward_tokens_sum);
if (prefill_tokens_sum != 0) {
metrics.Set("prefill_tokens_per_s", prefill_tokens_sum / engine_prefill_time_sum);
}
if (engine_decode_time_sum != 0) {
metrics.Set("decode_tokens_per_s", decode_tokens_sum / engine_decode_time_sum);
}
metrics.Set("last_finished_request", last_finished_request.AsJSON());
if (!spec_decode.IsEmpty()) {
metrics.Set("spec_decode", spec_decode.AsJSON());
}
auto f_create_time_list = [](const std::vector<TimeCost>& time_list) {
tvm::ffi::json::Object result;
for (size_t i = 1; i < time_list.size(); ++i) {
const TimeCost& item = time_list[i];
if (item.count == 0) continue;
std::ostringstream label_mean;
label_mean << "mean{batch_size=" << i << "}";
double mean = item.sum / item.count;
result.Set(label_mean.str(), mean);
std::ostringstream label_count;
label_count << "count{batch_size=" << i << "}";
result.Set(label_count.str(), item.count);
}
return tvm::ffi::json::Value(result);
};
metrics.Set("decode_time_by_batch_size", f_create_time_list(decode_time_by_batch_size));
metrics.Set("draft_time_by_batch_size", f_create_time_list(draft_time_by_batch_size));
metrics.Set("verify_time_by_batch_size", f_create_time_list(verify_time_by_batch_size));
return metrics;
}
std::string EngineMetrics::AsUsageJSONStr() const {
tvm::ffi::json::Object usage;
// We return engine usage as a usage field according to the OpenAI API.
// To comply with the API, just set prompt_tokens, completion_tokens, and total_tokens to 0.
// And store the information in the extra field.
usage.Set("prompt_tokens", static_cast<int64_t>(0));
usage.Set("completion_tokens", static_cast<int64_t>(0));
usage.Set("total_tokens", static_cast<int64_t>(0));
usage.Set("extra", this->AsJSON());
return tvm::ffi::json::Stringify(usage);
}
void EngineMetrics::Reset() {
engine_prefill_time_sum = 0.0;
engine_decode_time_sum = 0.0;
engine_jump_forward_time_sum = 0;
prompt_tokens_sum = 0;
completion_tokens_sum = 0;
prefill_tokens_sum = 0;
decode_tokens_sum = 0;
jump_forward_tokens_sum = 0;
last_finished_request.Reset();
spec_decode.Reset();
decode_time_by_batch_size.clear();
draft_time_by_batch_size.clear();
verify_time_by_batch_size.clear();
decode_time_by_batch_size.resize(kEndFineGrainedTrackingBatchSize);
draft_time_by_batch_size.resize(kEndFineGrainedTrackingBatchSize);
verify_time_by_batch_size.resize(kEndFineGrainedTrackingBatchSize);
}
} // namespace serve
} // namespace llm
} // namespace mlc
+264
View File
@@ -0,0 +1,264 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/metric.h
* \brief Metrics of serving engine/requests.
*/
#ifndef MLC_LLM_SERVE_METRICS_H_
#define MLC_LLM_SERVE_METRICS_H_
#include <tvm/ffi/extra/json.h>
#include <tvm/runtime/logging.h>
#include <chrono>
#include <string>
namespace mlc {
namespace llm {
namespace serve {
// We keep all metrics containers in this header (instead of in Engine and Request State)
// so we have a single central place to define all metrics across the engine.
// Conceptually, these statistics are derived from engine/request behaviors.
/*!
* \brief The class for tracking mean time cost.
* - We maintain the number of updates (`count`) and the sum of updated values (`sum`).
* - We support warmup. When `warmup` is false, the first update will be discarded.
*/
struct TimeCost {
/*! \brief the total amount of cost excluding warm up time */
double sum = 0.0;
/*! \brief the total count of events excluding warmup */
int64_t count = 0;
/*! \brief Whether we warmed up already, assuming one hit is enough */
bool warmed_up = false;
/*! \brief Update the metric with given value. */
void Update(double value) {
if (warmed_up) {
sum += value;
count += 1;
} else {
warmed_up = true;
}
}
/*! \brief Reset the metric. */
void Reset() {
// NOTE: no need to redo warmup
// assuming we are measuring the same thing
this->sum = 0.0;
this->count = 0;
}
/*! \brief Dump the metric as JSON. */
tvm::ffi::json::Object AsJSON() const;
};
/*! \brief Runtime metrics for speculative decoding */
struct SpecDecodeMetrics {
/*! \brief The number of draft tokens in speculative decoding, per step */
std::vector<int64_t> draft_count;
/*! \brief The number of accepted tokens in speculative decoding, per step */
std::vector<int64_t> accept_count;
/*!
* \brief Update the metrics of speculative decoding.
* \param draft_length The number of draft tokens (including the last prediction by the base
* model)
* \param accept_length The number of accepted tokens in the speculative decoding.
*/
void Update(int draft_length, int accept_length) {
TVM_FFI_ICHECK_GE(accept_length, 1);
if (accept_count.size() < draft_length) {
this->accept_count.resize(draft_length, 0);
this->draft_count.resize(draft_length, 0);
}
for (int j = 0; j < draft_length; ++j) {
if (j < accept_length) {
++this->accept_count[j];
}
++this->draft_count[j];
}
}
bool IsEmpty() const { return draft_count.size() == 0; }
void Reset() {
accept_count.clear();
draft_count.clear();
}
tvm::ffi::json::Object AsJSON() const;
};
/*!
* \brief Metrics attached to each request
*
* Sometimes requests can involve tree decode(e.g. parallel n).
* The metrics is collected across all branches of the tree.
*/
struct RequestMetrics {
/*! \brief Request input tokens. */
int64_t prompt_tokens = 0;
/*! \brief Total number of output tokens. */
int64_t completion_tokens = 0;
/*! \brief Total number of tokens that needs to be prefilled */
int64_t prefill_tokens = 0;
/*! \brief The number of processed tokens (including tokens rolled back later) in decode. */
int64_t decode_tokens = 0;
/*! \brief The number of tokens predicted by jump-forward decoding. */
int64_t jump_forward_tokens = 0;
/*! \brief The time of adding the request to engine. */
std::chrono::high_resolution_clock::time_point add_time_point;
/*! \brief The time of finishing prefill stage. */
std::chrono::high_resolution_clock::time_point prefill_end_time_point;
/*! \brief The time of finishing all decode. */
std::chrono::high_resolution_clock::time_point finish_time_point;
/*! \brief check whether the request metrics is a completed request */
bool IsComplete() const { return prompt_tokens != 0 && completion_tokens != 0; }
/*! \return the prefill time in seconds */
double GetPrefillTime() const {
return static_cast<double>((prefill_end_time_point - add_time_point).count()) / 1e9;
}
/*! \return the decode time in seconds */
double GetDecodeTime() const {
return static_cast<double>((finish_time_point - prefill_end_time_point).count()) / 1e9;
}
/*! \return the time to first token (TTFT) in seconds */
double GetTTFT() const {
return static_cast<double>((prefill_end_time_point - add_time_point).count()) / 1e9;
}
/*! \return the prefill time in seconds */
double GetTotalTime() const {
return static_cast<double>((finish_time_point - add_time_point).count()) / 1e9;
}
/*! \return the inter token latency (ITL) in seconds */
double GetInterTokenLatency() const {
return completion_tokens > 0 ? GetTotalTime() / completion_tokens : 0.0;
}
/*! \brief Reset the metric. */
void Reset() {
this->prompt_tokens = 0;
this->prefill_tokens = 0;
this->completion_tokens = 0;
}
/*!
* \brief Return the request metrics in JSON.
* \return The metrics in JSON
*/
tvm::ffi::json::Object AsJSON() const;
/*!
* \brief Return OpenAI compatible usage metrics
* \param include_extra Whether to include extra set of metrics
*
* \return The usage metrics in json.
*/
std::string AsUsageJSONStr(bool include_extra) const;
};
/*! \brief Runtime metrics of engine. */
struct EngineMetrics {
/*! \brief The total engine time on prefill, including warmup */
double engine_prefill_time_sum = 0;
/*! \brief The total engine time on decode/draft/verify, including warmup */
double engine_decode_time_sum = 0;
/*! \brief The total engine time on jump-forward prediction. */
double engine_jump_forward_time_sum = 0;
/*! \brief The total number of request input tokens. */
int64_t prompt_tokens_sum = 0;
/*! \brief The total number of request output tokens */
int64_t completion_tokens_sum = 0;
/*! \brief The total number of processed tokens (excluding the prefix-cached length) in prefill */
int64_t prefill_tokens_sum = 0;
/*! \brief The total number of processed tokens (including tokens rolled back later) in decode. */
int64_t decode_tokens_sum = 0;
/*! \brief The total number of tokens predicted by jump-forward decoding. */
int64_t jump_forward_tokens_sum = 0;
/*! \brief metrics from last finished request. */
RequestMetrics last_finished_request;
/*! \brief speculative decoding metrics */
SpecDecodeMetrics spec_decode;
/*! \brief The maximum batch size we track for batch decode time. */
static constexpr const int64_t kEndFineGrainedTrackingBatchSize = 65;
/*! \brief The list of batch decode time under different batch size. */
std::vector<TimeCost> decode_time_by_batch_size =
std::vector<TimeCost>(kEndFineGrainedTrackingBatchSize);
/*! \brief The list of batch draft time (a single decode step) under different batch size. */
std::vector<TimeCost> draft_time_by_batch_size =
std::vector<TimeCost>(kEndFineGrainedTrackingBatchSize);
/*! \brief The list of batch verification time under different effective batch size. */
std::vector<TimeCost> verify_time_by_batch_size =
std::vector<TimeCost>(kEndFineGrainedTrackingBatchSize);
// NOTE: we keep most update function in header
// so they can be inlined effectively
/*!
* \brief Update the batch decode time for the given batch size.
* The time will be ignored if the batch size is greater than `kMaxBatchSizeForTracking`.
*/
void UpdateDecodeTimeByBatchSize(int batch_size, double time) {
if (batch_size < kEndFineGrainedTrackingBatchSize) {
decode_time_by_batch_size[batch_size].Update(time);
}
}
/*!
* \brief Update the single-step batch draft time for the given batch size.
* The time will be ignored if the batch size is greater than `kMaxBatchSizeForTracking`.
*/
void UpdateDraftTimeByBatchSize(int batch_size, double time) {
if (batch_size < kEndFineGrainedTrackingBatchSize) {
draft_time_by_batch_size[batch_size].Update(time);
}
}
/*!
* \brief Update the batch decode time for the given effective batch sizPe.
* The time will be ignored if the effective batch size is greater than
* `kMaxBatchSizeForTracking`.
*/
void UpdateVerifyTimeByBatchSize(int effective_batch_size, double time) {
if (effective_batch_size < kEndFineGrainedTrackingBatchSize) {
verify_time_by_batch_size[effective_batch_size].Update(time);
}
}
/*!
* \brief Update global engine metrics as we finish a request
* by including the information from the finished request.
*/
void RequestFinishUpdate(const RequestMetrics& request_metrics) {
prompt_tokens_sum += request_metrics.prompt_tokens;
prefill_tokens_sum += request_metrics.prefill_tokens;
completion_tokens_sum += request_metrics.completion_tokens;
decode_tokens_sum += request_metrics.decode_tokens;
jump_forward_tokens_sum += request_metrics.jump_forward_tokens;
last_finished_request = request_metrics;
}
/*!
* \brief Return the engine runtime metrics in JSON.
* \return The metrics in JSON
*/
tvm::ffi::json::Object AsJSON() const;
/*!
* \brief return engine metrics as usage json string.
* \return The resulting usage json string.
*/
std::string AsUsageJSONStr() const;
/*! \brief Reset all the metrics. */
void Reset();
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_METRIC_H_
+1293
View File
File diff suppressed because it is too large Load Diff
+418
View File
@@ -0,0 +1,418 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/model.h
* \brief The header for runtime module of LLM functions (prefill/decode/etc.)
*/
#ifndef MLC_LLM_SERVE_MODEL_H_
#define MLC_LLM_SERVE_MODEL_H_
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/string.h>
#include <tvm/runtime/tensor.h>
#include "../base.h"
#include "../support/result.h"
#include "config.h"
#include "draft_token_workspace_manager.h"
#include "event_trace_recorder.h"
#include "function_table.h"
#include "logit_processor.h"
#include "sampler/sampler.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::Device;
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
using tvm::ffi::Shape;
// Declare the sampler class for `Model::CreateSampler`.
class Sampler;
/*!
* \brief The workspace tensors that may be shared across different
* calls to Model. For example, the prefill action use the `embeddings`
* workspace for the concatenated embeddings of different sequences.
* The workspace tensor is created by Model but owned by engine.
*/
struct ModelWorkspace {
/*!
* \brief The embedding tensor. It can be either an Tensor when tensor
* model parallelism is not enabled, or a DRef when using tensor model parallelism.
*/
ObjectRef embeddings{nullptr};
/*!
* \brief The hidden_states tensor for the current batch. It can be either an Tensor when tensor
* model parallelism is not enabled, or a DRef when using tensor model parallelism.
*/
ObjectRef hidden_states{nullptr};
/*!
* \brief The draft token probabilities tensor for the current batch.
*/
Tensor draft_probs{nullptr};
/*!
* \brief The hidden_states tensor storing the hidden_states of draft tokens of all requests.
*/
ObjectRef draft_hidden_states_storage{nullptr};
/*!
* \brief The draft token probabilities tensor storing the probabilities of draft tokens of all
* requests.
*/
Tensor draft_probs_storage{nullptr};
};
/*!
* \brief The model module for LLM functions.
* It runs an LLM, and has an internal KV cache that maintains
* the history KV values of all processed tokens.
*
* It contains the following functions:
*
* Model related:
* - "token_embed": take token ids as input and return the embeddings,
* - "batch_prefill": take embedding of a single sequence
* as input, forward the embedding through LLM and return the logits,
* - "decode": take the embeddings of the last-committed token of an
* entire batch as input, forward through LLM and return the logits
* for all sequences in the batch,
* - "softmax_with_temperature": take logits and temperatures, return
* probabilities.
*
* KV cache related:
* - "create_kv_cache": create the KV cache for this module,
* - "add_new_sequence": add (declare) a new sequence in the KV cache,
* - "remove_sequence": remove a sequence from KV cache.
*
* ... and some other auxiliary functions.
*/
class ModelObj : public Object {
public:
/*********************** Model Computation ***********************/
/*!
* \brief Compute embeddings for the input token ids.
* When the input destination pointer is defined, it in-place writes the
* embedding into the input destination array at the given offset.
* Otherwise, the embeddings will be directly returned back.
* \param token_ids The token ids to compute embedding for.
* \param dst The destination array of the embedding lookup.
* \param offset The token offset where the computed embeddings will be written
* into the destination array.
* \return The updated destination embedding array or the computed embeddings.
* \note When `dst` is undefined, we require `offset` to be 0.
*/
virtual ObjectRef TokenEmbed(Shape batch_token_ids, ObjectRef* dst = nullptr, int offset = 0) = 0;
/*!
* \brief Compute embeddings for the input image.
* \param image The image to compute embedding for.
* \return The computed embeddings.
*/
virtual ObjectRef ImageEmbed(const Tensor& image, ObjectRef* dst = nullptr, int offset = 0) = 0;
/*!
* \brief Fuse the embeddings and hidden_states.
* \param embeddings The embedding of the input to be prefilled.
* \param previous_hidden_states The hidden_states from previous base model.
* \param batch_size Batch size.
* \param seq_len Sequence length.
* \return The fused hidden_states.
*/
virtual ObjectRef FuseEmbedHidden(const ObjectRef& embeddings,
const ObjectRef& previous_hidden_states, int batch_size,
int seq_len) = 0;
/*!
* \brief Return if the model has lm_head so that we can get logits.
*/
virtual bool CanGetLogits() = 0;
/*!
* \brief Compute logits for last hidden_states.
* \param last_hidden_states The last hidden_states to compute logits for.
* \return The computed logits.
*/
virtual Tensor GetLogits(const ObjectRef& last_hidden_states) = 0;
virtual Array<Tensor> GetMultiStepLogits(const ObjectRef& last_hidden_states) = 0;
/*!
* \brief Batch prefill function. Embedding in, logits out.
* The embedding order of sequences in `embedding_arr` follows
* the order of `seq_ids`.
* \param embeddings The embedding of the input to be prefilled.
* \param seq_id The id of the sequence in the KV cache.
* \param lengths The length of each sequence to prefill.
* \return The logits for the next token.
*/
virtual Tensor BatchPrefill(const ObjectRef& embeddings, const std::vector<int64_t>& seq_ids,
const std::vector<int>& lengths) = 0;
/*!
* \brief Batch prefill function. Input hidden_states are computed from
* input embeddings and previous hidden_states, output last hidden_states.
* \param hidden_states The hidden_states of the input to be prefilled.
* \param seq_id The id of the sequence in the KV cache.
* \param lengths The length of each sequence to prefill.
* \return The hidden_states for the next token.
*/
virtual ObjectRef BatchPrefillToLastHidden(const ObjectRef& hidden_states,
const std::vector<int64_t>& seq_ids,
const std::vector<int>& lengths) = 0;
/*!
* \brief Batch decode function. Embedding in, logits out.
* The embedding order of sequences in `embeddings` follows
* the order of `seq_ids`.
* \param embeddings The embedding of last generated token in the entire batch.
* \param seq_id The id of the sequence in the KV cache.
* \return The logits for the next token for each sequence in the batch.
*/
virtual Tensor BatchDecode(const ObjectRef& embeddings, const std::vector<int64_t>& seq_ids) = 0;
virtual Tensor BatchTreeDecode(const ObjectRef& embeddings, const std::vector<int64_t>& seq_ids,
const std::vector<int>& lengths,
const std::vector<int64_t>& token_tree_parent_ptr) = 0;
/*!
* \brief Batch decode function. Input hidden_states are computed from
* input embeddings and previous hidden_states, output last hidden_states.
* \param hidden_states The hidden_states of last generated token in the entire batch.
* \param seq_id The id of the sequence in the KV cache.
* \return The hidden_states for the next token for each sequence in the batch.
*/
virtual ObjectRef BatchDecodeToLastHidden(const ObjectRef& hidden_states,
const std::vector<int64_t>& seq_ids) = 0;
/*!
* \brief Batch verify function. Embedding in, logits out.
* \param embeddings The embedding of the input to be verified.
* \param seq_id The id of the sequence in the KV cache.
* \param lengths The length of each sequence to verify.
* \param token_tree_parent_ptr The parent pointers of the token tree.
* It's size is the sum of "lengths". It contains a batch of independent trees,
* one for each sequence. Parent being "-1" means the node is a root.
* \return The logits for the draft token for each sequence in the batch.
* \note The function runs for **every** sequence in the batch.
* That is to say, it does not accept "running a verify step for a subset
* of the full batch".
*/
virtual Tensor BatchVerify(const ObjectRef& embeddings, const std::vector<int64_t>& seq_ids,
const std::vector<int>& lengths,
const std::vector<int64_t>& token_tree_parent_ptr) = 0;
/*!
* \brief Batch verify function. Input hidden_states are computed from
* input embeddings and previous hidden_states, output last hidden_states.
* \param hidden_states The hidden_states of the input to be verified.
* \param seq_id The id of the sequence in the KV cache.
* \param lengths The length of each sequence to verify.
* \param token_tree_parent_ptr The parent pointers of the token tree.
* It's size is the sum of "lengths". It contains a batch of independent trees,
* one for each sequence. Parent being "-1" means the node is a root.
* \return The hidden_states for the draft token for each sequence in the batch.
* \note The function runs for **every** sequence in the batch.
* That is to say, it does not accept "running a verify step for a subset
* of the full batch".
*/
virtual ObjectRef BatchVerifyToLastHidden(const ObjectRef& hidden_states,
const std::vector<int64_t>& seq_ids,
const std::vector<int>& lengths,
const std::vector<int64_t>& token_tree_parent_ptr) = 0;
/*********************** KV Cache Management ***********************/
/*!
* \brief Create the KV cache inside the model with regard to the input config.
* \param page_size The number of consecutive tokens handled in each page in paged KV cache.
* \param max_num_sequence The maximum number of sequences that are allowed to be
* processed by the KV cache at any time.
* \param max_total_sequence_length The maximum length allowed for a single sequence
* in the engine.
* \param prefill_chunk_size The maximum total number of tokens whose KV data
* are allowed to exist in the KV cache at any time.
* \param max_history_size The maximum history size for RNN state to roll back.
* The KV cache does not need this.
* \param prefix_cache_max_num_recycling_seqs The maximum number of recycling
* sequences kept by prefix cache. For hybrid models, the RNN state needs
* extra slots beyond max_num_sequence to hold these recycling sequences
* simultaneously with active sequences.
*/
virtual void CreateKVCache(int page_size, int max_num_sequence, int64_t max_total_sequence_length,
int64_t prefill_chunk_size, int max_history_size,
int prefix_cache_max_num_recycling_seqs = 0) = 0;
/*! \brief Add a new sequence with the given sequence id to the KV cache. */
virtual void AddNewSequence(int64_t seq_id) = 0;
/*! \brief Fork a sequence from a given parent sequence. */
virtual void ForkSequence(int64_t parent_seq_id, int64_t child_seq_id, int64_t fork_pos = -1) = 0;
/*! \brief Remove the given sequence from the KV cache in the model. */
virtual void RemoveSequence(int64_t seq_id) = 0;
/*! \brief Pop out N pages from KV cache. */
virtual void PopNFromKVCache(int64_t seq_id, int num_tokens) = 0;
/*!
* \brief Commit the accepted token tree nodes to KV cache.
* The unaccepted token tree node will be removed from KV cache.
* This is usually used in the verification stage of speculative decoding.
*/
virtual void CommitAcceptedTokenTreeNodesToKVCache(
const std::vector<int64_t>& seq_ids, const std::vector<int64_t>& accepted_leaf_indices) = 0;
/*!
* \brief Enabling sliding window for the given sequence.
* It is a no-op if the model does not support sliding window.
* \note Given this operation is tied with the underlying KV cache,
* we add the function in Model interface to expose this for Engine.
* This may be optimized with decoupling KV cache and Model in the future.
*/
virtual void EnableSlidingWindowForSeq(int64_t seq_id) = 0;
/*! \brief Prepare for the disaggregation KV data receive for the specified sequence and length.*/
virtual Shape DisaggPrepareKVRecv(int64_t seq_id, int length) = 0;
/*! \brief Prepare for the disaggregation KV data send for the specified sequence and length.*/
virtual void DisaggMarkKVSend(int64_t seq_id, int begin_pos, Shape compressed_kv_append_metadata,
int dst_group_offset) = 0;
/************** Raw Info Query **************/
/*! \brief Return the metadata JSON object of the model. */
virtual ModelMetadata GetMetadata() const = 0;
/*! \brief Get the number of available pages in KV cache. */
virtual int GetNumAvailablePages() const = 0;
/*! \brief Get the current total sequence length in the KV cache. */
virtual int GetCurrentTotalSequenceLength() const = 0;
/*********************** Utilities ***********************/
/*! \brief Load the model's weight parameters, which is not loaded at construction time. */
virtual void LoadParams() = 0;
/*!
* \brief Set the maximum number of sequences to be processed for the model,
* which is not initialized at construction time.
*/
virtual void SetMaxNumSequence(int max_num_sequence) = 0;
/*!
* \brief Set the prefill chunk size for the model,
* which is not initialized at construction time.
*/
virtual void SetPrefillChunkSize(int prefill_chunk_size) = 0;
/*! \brief Create a logit processor from this model. */
virtual LogitProcessor CreateLogitProcessor(int max_num_token,
Optional<EventTraceRecorder> trace_recorder) = 0;
/*! \brief Create a sampler from this model. */
virtual Sampler CreateSampler(int max_num_sample, int num_models,
Optional<EventTraceRecorder> trace_recorder) = 0;
/*!
* \brief Estimate number of CPU units required to drive the model
* executing during TP.
* \note This normally equals to the number of TP shards (or 0 if
* the model does not use TP) and can be used to hint runtime to
* avoid overuse cores in other places.
*/
virtual int EstimateHostCPURequirement() const = 0;
/*! \brief Get the sliding window size of the model. "-1" means sliding window is not enabled. */
virtual int GetSlidingWindowSize() const = 0;
/*! \brief Get the attention sink size of the model. */
virtual int GetAttentionSinkSize() const = 0;
/*! \brief Allocate an embedding tensor with the prefill chunk size. */
virtual ObjectRef AllocEmbeddingTensor() = 0;
/*! \brief Allocate an hidden_states tensor with the prefill chunk size. */
virtual ObjectRef AllocHiddenStatesTensor() = 0;
/*! \brief Reset the model KV cache and other metrics. */
virtual void Reset() = 0;
/*********************** Utilities for speculative decoding. ***********************/
virtual DraftTokenWorkspaceManager CreateDraftTokenWorkspaceManager(int max_num_token) = 0;
/*! \brief Gather the hidden_states of the given indices and in-place update the dst tensor. */
virtual ObjectRef GatherHiddenStates(const ObjectRef& input, const std::vector<int>& indices,
ObjectRef* dst) = 0;
/*! \brief Scatter the hidden_states of the given indices to the dst tensor. */
virtual void ScatterHiddenStates(const ObjectRef& input, const std::vector<int>& indices,
ObjectRef* dst) = 0;
/*! \brief Gather the draft token probabilities of the given indices and in-place update the dst
* tensor. */
virtual Tensor GatherDraftProbs(const Tensor& input, const std::vector<int>& indices,
Tensor* dst) = 0;
/*! \brief Scatter the draft token probabilities of the given indices to the dst tensor. */
virtual void ScatterDraftProbs(const Tensor& input, const std::vector<int>& indices,
Tensor* dst) = 0;
/************** Debug/Profile **************/
/*! \brief Call the given global function on all workers. Only for debug purpose. */
virtual void DebugCallFuncOnAllAllWorker(const String& func_name, Optional<String> func_args) = 0;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<ModelObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.Model", ModelObj, Object);
};
class Model : public ObjectRef {
public:
/*!
* \brief Create the runtime module for LLM functions.
* \param reload_lib_path The model library path.
* \param model_path The path to the model weight parameters.
* \param model_config The model config json object.
* \param device The device to run the model on.
* \param session The session to run the model on.
* \param num_shards The number of tensor parallel shards of the model.
* \param num_stages The number of pipeline parallel stages of the model.
* \param trace_enabled A boolean indicating whether tracing is enabled.
* \return The created runtime module.
*/
static Model Create(String reload_lib_path, String model_path,
const tvm::ffi::json::Object& model_config, DLDevice device,
const Optional<Session>& session, int num_shards, int num_stages,
bool trace_enabled);
/*!
* Load the model config from the given model path.
* \param model_path The path to the model weight parameters.
* \return The model config json object.
*/
static Result<tvm::ffi::json::Object> LoadModelConfig(const String& model_path);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(Model, ObjectRef, ModelObj);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_MODEL_H_
+442
View File
@@ -0,0 +1,442 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/prefix_cache.cc
*/
#include "prefix_cache.h"
#include <tvm/ffi/function.h>
#include <tvm/support/cuda/nvtx.h>
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
using tvm::support::NVTXScopedRange;
TVM_FFI_STATIC_INIT_BLOCK() { PrefixCacheObj::RegisterReflection(); }
/*!
* \brief The implementation of prefix cache.
*/
class PrefixCacheImpl : public PrefixCacheObj {
public:
/*!
* \brief Constructor of paged radix tree.
* \param max_num_recycling_seqs The maximum number of sequences in prefix cache.
* \param remove_callback The optional callback function to call when removing a sequence.
*/
explicit PrefixCacheImpl(size_t max_num_recycling_seqs, PrefixCacheRemoveCallback remove_callback)
: radix_tree_(PagedRadixTree::Create()),
max_num_recycling_seqs_(max_num_recycling_seqs),
remove_callback_(std::move(remove_callback)) {
recycling_seq_lrus_.clear();
reversed_recycling_seq_lrus_.clear();
seq_states_.clear();
seq_sliding_window_infos_.clear();
lru_counter_ = 0;
}
/*!
* \brief Insert a new tokenized sequence into Prefix Cache.
* \param seq_id The sequence ID.
* \param tokens The tokens of tokenized sequence.
* \param sliding_window_size The sliding window size for the sequence, -1 as sliding window
* disabled.
* \param attention_sink_size The attention sink size for the sequence, 0 by default.
* \return The matched result.
*/
PrefixCacheMatchedResult InsertSequence(int64_t seq_id, std::vector<int32_t> tokens,
int sliding_window_size, int attention_sink_size) final {
TVM_FFI_ICHECK_NE(sliding_window_size, 0);
TVM_FFI_ICHECK_GE(attention_sink_size, 0);
TVM_FFI_ICHECK(seq_states_.find(seq_id) == seq_states_.end());
TVM_FFI_ICHECK(seq_sliding_window_infos_.find(seq_id) == seq_sliding_window_infos_.end());
TVM_FFI_ICHECK(!tokens.empty());
CommitSequenceExtention();
tokens.pop_back();
auto [matched_offset, matched_seqs] = radix_tree_->MatchPrefix(tokens);
std::pair<int, size_t> sliding_window_info{sliding_window_size, attention_sink_size};
// No prefix matched, directly adding new sequence.
if (!matched_offset) {
radix_tree_->AddSequence(seq_id);
seq_states_.emplace(seq_id, SequenceState::kActive);
seq_sliding_window_infos_.emplace(seq_id, sliding_window_info);
return PrefixCacheMatchedResult{0, -1, -1, 0};
}
TVM_FFI_ICHECK(!matched_seqs.empty());
// The reusage of recycling sequences logic is different between with/without sliding window
// enabled.
if (sliding_window_size != -1) {
// If sliding window enabled, the reusage of recycling sequences should be limited to exactly
// matched. And no rolling back is allowed due to the sliding window.
for (int64_t matched_seq_id : matched_seqs) {
if (seq_states_.at(matched_seq_id) == SequenceState::kRecycling &&
seq_sliding_window_infos_.at(matched_seq_id) == sliding_window_info) {
size_t matched_seq_length = radix_tree_->GetSequenceLength(matched_seq_id);
if (matched_seq_length == matched_offset) {
ReuseRecyclingSequence(matched_seq_id);
return PrefixCacheMatchedResult{matched_offset, -1, matched_seq_id, 0};
}
}
}
} else {
// If sliding window is not enabled, we can greedily reuse the shortest recycling sequence
// without sliding window, so that the loss or roll back of trailing tokens will be minimum.
size_t shortest_recycling_seq_length = 0;
int64_t shortest_recycling_seq_id = -1;
for (int64_t matched_seq_id : matched_seqs) {
if (seq_states_.at(matched_seq_id) == SequenceState::kRecycling &&
seq_sliding_window_infos_.at(matched_seq_id) == sliding_window_info) {
size_t matched_seq_length = radix_tree_->GetSequenceLength(matched_seq_id);
if (shortest_recycling_seq_id == -1 ||
matched_seq_length < shortest_recycling_seq_length) {
shortest_recycling_seq_id = matched_seq_id;
shortest_recycling_seq_length = matched_seq_length;
}
}
}
if (shortest_recycling_seq_id != -1 && matched_offset > shortest_recycling_seq_length * 0.9) {
ReuseRecyclingSequence(shortest_recycling_seq_id);
if (shortest_recycling_seq_length > matched_offset) {
// Recycling sequence is longer than new sequence, rolling back the redundant trailing
// tokens, to match the new sequence.
radix_tree_->RollBackSequence(shortest_recycling_seq_id,
shortest_recycling_seq_length - matched_offset);
}
return PrefixCacheMatchedResult{matched_offset, -1, shortest_recycling_seq_id,
shortest_recycling_seq_length - matched_offset};
}
// No reusage of recycling sequence, fallback to forking matched sequence. Currently, we only
// fork from sequence without sliding window, due to current paged KVCache implementation.
size_t longest_forking_offset = 0;
int64_t longest_forking_seq_id = -1;
for (int64_t matched_seq_id : matched_seqs) {
auto [matched_seq_sliding_window_size, matched_seq_attention_sink_size] =
seq_sliding_window_infos_.at(matched_seq_id);
if (matched_seq_sliding_window_size != -1) {
continue;
}
// If the matched is not enabled with sliding window, we can fork within matched offset
// tokens arbitrarily.
if (matched_offset > longest_forking_offset) {
longest_forking_offset = matched_offset;
longest_forking_seq_id = matched_seq_id;
}
}
if (longest_forking_offset > 0) {
radix_tree_->ForkSequence(seq_id, longest_forking_seq_id, longest_forking_offset);
seq_states_.emplace(seq_id, SequenceState::kActive);
seq_sliding_window_infos_.emplace(seq_id, sliding_window_info);
return PrefixCacheMatchedResult{longest_forking_offset, longest_forking_seq_id, -1, 0};
}
}
// No forking from matched sequence, fallback to adding new sequence.
radix_tree_->AddSequence(seq_id);
seq_states_.emplace(seq_id, SequenceState::kActive);
seq_sliding_window_infos_.emplace(seq_id, sliding_window_info);
return PrefixCacheMatchedResult{0, -1, -1, 0};
}
/*!
* \brief Extend a sequence with new tokenized sequence suffix.
* \param seq_id The sequence to be extended.
* \param tokens The tokens of tokenized sequence suffix to extend.
* \throw Error if the given sequence id is not valid or active.
*/
void ExtendSequence(int64_t seq_id, const std::vector<int32_t>& tokens) final {
uncommitted_extended_token_ids_.emplace_back(seq_id, tokens);
}
void CommitSequenceExtention() final {
if (uncommitted_extended_token_ids_.empty()) {
return;
}
NVTXScopedRange nvtx_scope("PrefixCache commit sequence extension");
for (const auto& [seq_id, uncommitted_token_ids] : uncommitted_extended_token_ids_) {
if (!HasSequence(seq_id)) {
// The sequence has been removed. Hence no action is needed.
continue;
}
const auto& it = seq_states_.find(seq_id);
TVM_FFI_ICHECK(it == seq_states_.end() || it->second == SequenceState::kActive);
radix_tree_->ExtendSequence(seq_id, uncommitted_token_ids);
}
uncommitted_extended_token_ids_.clear();
}
/*!
* \brief Roll back a sequence by number of tokens.
* \param seq_id The sequence ID for index.
* \param num_tokens The number of tokens to be rolled back.
* \throw Error if the given sequence id is not valid or active.
*/
void RollBackSequence(int64_t seq_id, size_t num_tokens) final {
CommitSequenceExtention();
TVM_FFI_ICHECK(seq_states_.at(seq_id) == SequenceState::kActive);
radix_tree_->RollBackSequence(seq_id, num_tokens);
}
/*!
* \brief Recycle a sequence. The recycled sequence will not be removed immediately, as long as
* memory is sufficient and the number of sequence in prefix cache belows the maximum number of
* sequence. And it will be reused again in the future request.
* \param seq_id The sequence to be recycled.
* \param lazy The flag if the sequence should be removed lazily or intermediary.
* \throw Error if the given sequence id is not valid.
*/
void RecycleSequence(int64_t seq_id, bool lazy = true) final {
CommitSequenceExtention();
TVM_FFI_ICHECK(seq_states_.at(seq_id) == SequenceState::kActive);
TVM_FFI_ICHECK(recycling_seq_lrus_.find(seq_id) == recycling_seq_lrus_.end());
if (lazy && max_num_recycling_seqs_ != 0) {
// Remove the sequence lazily.
if (recycling_seq_lrus_.size() == max_num_recycling_seqs_) {
// If prefix cache has reached maximum number of recycling sequences, try to pop one
// recycling sequence.
TVM_FFI_ICHECK(TryFreeMemory());
TVM_FFI_ICHECK_EQ(recycling_seq_lrus_.size(), max_num_recycling_seqs_ - 1);
}
seq_states_.at(seq_id) = SequenceState::kRecycling;
++lru_counter_;
recycling_seq_lrus_.emplace(seq_id, lru_counter_);
reversed_recycling_seq_lrus_.emplace(lru_counter_, seq_id);
} else {
// Remove the sequence intermediately.
radix_tree_->RemoveSequence(seq_id);
if (remove_callback_ != nullptr) {
remove_callback_(seq_id);
}
TVM_FFI_ICHECK(seq_states_.erase(seq_id));
TVM_FFI_ICHECK(seq_sliding_window_infos_.erase(seq_id));
}
}
/*!
* \brief Try to remove recycling sequence to free up memory. It will remove the oldest recycling
sequence.
* \return The flag if there is a sequence removed. In other word, return true when memory is
freed successfully.
* \throw Error if the given sequence id is not valid.
*/
bool TryFreeMemory() final {
NVTXScopedRange nvtx_scope("PrefixCache TryFreeMemory");
if (reversed_recycling_seq_lrus_.empty()) {
// There is no recycling sequence. No memory can be freed.
return false;
}
auto [lru, seq_id] = *reversed_recycling_seq_lrus_.begin();
TVM_FFI_ICHECK(seq_states_.at(seq_id) == SequenceState::kRecycling);
TVM_FFI_ICHECK_EQ(recycling_seq_lrus_.at(seq_id), lru);
radix_tree_->RemoveSequence(seq_id);
if (remove_callback_ != nullptr) {
remove_callback_(seq_id);
}
TVM_FFI_ICHECK(seq_states_.erase(seq_id));
TVM_FFI_ICHECK(recycling_seq_lrus_.erase(seq_id));
TVM_FFI_ICHECK(reversed_recycling_seq_lrus_.erase(lru));
TVM_FFI_ICHECK(seq_sliding_window_infos_.erase(seq_id));
return true;
}
/*!
* \brief Check if a sequence exists.
* \param seq_id The sequence ID for index.
* \return The sequence existence.
* \throw Error if sequence ID is not valid.
*/
bool HasSequence(int64_t seq_id) final { return radix_tree_->HasSequence(seq_id); }
/*!
* \brief Reset the prefix cache to initial status.
*/
void Reset() final {
radix_tree_->Reset();
recycling_seq_lrus_.clear();
reversed_recycling_seq_lrus_.clear();
seq_states_.clear();
seq_sliding_window_infos_.clear();
uncommitted_extended_token_ids_.clear();
lru_counter_ = 0;
}
PrefixCacheMode Mode() final { return PrefixCacheMode::kRadix; }
private:
void ReuseRecyclingSequence(int64_t seq_id) {
TVM_FFI_ICHECK(seq_states_.at(seq_id) == SequenceState::kRecycling);
size_t lru = recycling_seq_lrus_.at(seq_id);
TVM_FFI_ICHECK_EQ(reversed_recycling_seq_lrus_.at(lru), seq_id);
seq_states_.at(seq_id) = SequenceState::kActive;
TVM_FFI_ICHECK(recycling_seq_lrus_.erase(seq_id));
TVM_FFI_ICHECK(reversed_recycling_seq_lrus_.erase(lru));
}
/*!
* \brief The sequence states.
*/
enum class SequenceState : int {
/*!
* \brief The state of active sequence. In this state, the sequence can be forked only. When
* recycling a sequence, it will transfer to kRecycling.
*/
kActive = 0,
/*!
* \brief The state of recycling sequence. In this state, the sequence can be forked or be
* reused. And it will transfer to kActive only when reused.
*/
kRecycling = 1,
};
/*!
* \brief The core data structure radix tree.
*/
PagedRadixTree radix_tree_;
/*!
* \brief The map from sequence to LRU time stamps.
*/
std::unordered_map<int64_t, size_t> recycling_seq_lrus_;
/*!
* \brief The map from LRU time stamps to sequence, used to find the sequence with earliest LRU
* time stamp.
*/
std::unordered_map<size_t, int64_t> reversed_recycling_seq_lrus_;
/*!
* \brief The maximum number of recycling sequences in prefix cache. Set -1 as infinite prefix
* cache.
*/
int max_num_recycling_seqs_ = -1;
/*!
* \brief The LRU counter.
*/
size_t lru_counter_ = 0;
/*!
* \brief The callback function to call when removing a sequence. This can be used to
* removing sequence in KVCache and return sequence ID to ID manager lazily
*/
PrefixCacheRemoveCallback remove_callback_ = nullptr;
/*!
* \brief The map from sequence to its sequence states.
*/
std::unordered_map<int64_t, SequenceState> seq_states_;
/*!
* \brief The map from sequence to its sliding window information. The sliding window information
* is a pair of sliding window size and attention sink size. The sliding window size is -1 for
* sliding window disabled, or positive for sliding window size. The attention sink size is
* non-negative and used when sliding window size is positive.
*/
std::unordered_map<int64_t, std::pair<int, size_t>> seq_sliding_window_infos_;
/*!
* \brief The collection of uncommitted extended token ids of sequences.
* The "ExtendSequence" method only lazily add token ids into this collection,
* and these uncommitted token ids will be committed when needed.
*
* Note: Since the tokens stored are references, CommitSequenceExtention should be called after
* each action, to avoid the uncaught changes of uncomitted extended token ids.
*/
std::vector<std::pair<int64_t, const std::vector<int32_t>&>> uncommitted_extended_token_ids_;
}; // namespace serve
/*!
* \brief The implementation of no prefix cache.
*/
class NoPrefixCache : public PrefixCacheObj {
public:
/*!
* \brief Insert a new tokenized sequence into Prefix Cache.
* \param seq_id The sequence ID.
* \param tokens The tokens of tokenized sequence.
* \param sliding_window_size The sliding window size for the sequence, -1 as sliding window
* disabled.
* \param attention_sink_size The attention sink size for the sequence, 0 by default.
* \return The matched result.
*/
PrefixCacheMatchedResult InsertSequence(int64_t seq_id, std::vector<int32_t> tokens,
int sliding_window_size, int attention_sink_size) final {
// Since there is no prefix cache, always return as new sequence.
return PrefixCacheMatchedResult{0, -1, -1, 0};
}
/*!
* \brief Extend a sequence with new tokenized sequence suffix.
* \param seq_id The sequence to be extended.
* \param tokens The tokens of tokenized sequence suffix to extend.
* \throw Error if called since this should never be called.
*/
void ExtendSequence(int64_t seq_id, const std::vector<int32_t>& tokens) final {
// No-op;
}
void CommitSequenceExtention() final {
// No-op;
}
/*!
* \brief Roll back a sequence by number of tokens.
* \param seq_id The sequence ID for index.
* \param num_tokens The number of tokens to be rolled back.
* \throw Error if called since this should never be called.
*/
void RollBackSequence(int64_t seq_id, size_t num_tokens) final {
// Since there is no prefix cache, this method should never be called.
LOG(FATAL) << "Unreachable code.";
}
/*!
* \brief Recycle a sequence. The recycled sequence will not be removed immediately, as long as
* memory is sufficient and the number of sequence in prefix cache belows the maximum number of
* sequence. And it will be reused again in the future request.
* \param seq_id The sequence to be recycled.
* \param lazy The flag if the sequence should be removed lazily or intermediary.
* \throw Error if the given sequence id is not valid.
*/
void RecycleSequence(int64_t seq_id, bool lazy = true) final {
// Since there is no prefix cache, this method should never be called.
LOG(FATAL) << "Unreachable code.";
}
/*!
* \brief Try to remove recycling sequence to free up memory. It will remove the oldest
recycling sequence.
* \return Always return false as no sequence stored.
*/
bool TryFreeMemory() final {
// Since there is no prefix cache, always return false.
return false;
}
/*!
* \brief Check if a sequence exists.
* \param seq_id The sequence ID for index.
* \return Always return false as no sequence stored.
*/
bool HasSequence(int64_t seq_id) final {
// Since there is no prefix cache, always return false.
return false;
}
/*!
* \brief Reset the prefix cache to initial status. Do nothing and return.
*/
void Reset() final {}
PrefixCacheMode Mode() final { return PrefixCacheMode::kDisable; }
};
PrefixCache PrefixCache::CreateRadixPrefixCache(size_t max_num_recycling_seqs,
PrefixCacheRemoveCallback remove_callback) {
ObjectPtr<PrefixCacheImpl> n =
tvm::ffi::make_object<PrefixCacheImpl>(max_num_recycling_seqs, std::move(remove_callback));
return PrefixCache(std::move(n));
}
PrefixCache PrefixCache::CreateNoPrefixCache() {
ObjectPtr<NoPrefixCache> n = tvm::ffi::make_object<NoPrefixCache>();
return PrefixCache(std::move(n));
}
} // namespace serve
} // namespace llm
} // namespace mlc
+158
View File
@@ -0,0 +1,158 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/prefix_cache.h
*/
#ifndef MLC_LLM_SERVE_PREFIX_CACHE_H_
#define MLC_LLM_SERVE_PREFIX_CACHE_H_
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/reflection/registry.h>
#include <functional>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include "model.h"
#include "radix_tree.h"
#include "request_state.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
/*!
* \brief The signature of callback removing function.
*/
using PrefixCacheRemoveCallback = std::function<void(int64_t)>;
/*!
* \brief The matched result from prefix cache. This result describes how to pre-process the new
* sequence, to leverage the existing data in KVCache by reusing past sequences or forking from
* other sequences.
*/
class PrefixCacheMatchedResult {
public:
/*!
* \brief The matched and prefilled prefix offset.
*/
size_t prefilled_offset = 0;
/*!
* \brief The sequence ID to fork from.
*/
int64_t forked_seq_id = -1;
/*!
* \brief The finished sequence ID to reuse.
*/
int64_t reused_seq_id = -1;
/*!
* \brief The number of tailing tokens to be popped from the reused sequence.
*/
size_t reused_seq_pop_last_tokens = 0;
};
class PrefixCacheObj : public Object {
public:
/*!
* \brief Insert a new tokenized sequence into Prefix Cache.
* \param seq_id The sequence ID.
* \param tokens The tokens of tokenized sequence.
* \param sliding_window_size The sliding window size for the sequence, -1 as sliding window
* disabled.
* \param attention_sink_size The attention sink size for the sequence, 0 by default.
* \return The matched result.
*/
virtual PrefixCacheMatchedResult InsertSequence(int64_t seq_id, std::vector<int32_t> tokens,
int sliding_window_size = -1,
int attention_sink_size = 0) = 0;
/*!
* \brief Extend a sequence with new tokenized sequence suffix.
* This extension might be cached and lazily committed later.
* \param seq_id The sequence to be extended.
* \param tokens The tokens of tokenized sequence suffix to extend.
* \throw Error if the given sequence id is not valid or active.
*/
virtual void ExtendSequence(int64_t seq_id, const std::vector<int32_t>& tokens) = 0;
/*! \brief Commit the cached sequence extension from "ExtendSequence". */
virtual void CommitSequenceExtention() = 0;
/*!
* \brief Roll back a sequence by number of tokens.
* \param seq_id The sequence ID for index.
* \param num_tokens The number of tokens to be rolled back.
* \throw Error if the given sequence id is not valid or active.
*/
virtual void RollBackSequence(int64_t seq_id, size_t num_tokens) = 0;
/*!
* \brief Recycle a sequence. The recycled sequence will not be removed immediately, as long as
* memory is sufficient and the number of sequence in prefix cache belows the maximum number of
* sequence. And it will be reused again in the future request.
* \param seq_id The sequence to be recycled.
* \param lazy The flag if the sequence should be removed lazily or intermediary.
* \throw Error if the given sequence id is not valid.
*/
virtual void RecycleSequence(int64_t seq_id, bool lazy = true) = 0;
/*!
* \brief Try to remove recycling sequence to free up memory. It will remove the oldest recycling
sequence.
* \return The flag if there is a sequence removed. In other word, return true when memory is
freed successfully.
* \throw Error if the given sequence id is not valid.
*/
virtual bool TryFreeMemory() = 0;
/*!
* \brief Check if a sequence exists.
* \param seq_id The sequence ID for index.
* \return The sequence existence.
* \throw Error if sequence ID is not valid.
*/
virtual bool HasSequence(int64_t seq_id) = 0;
/*!
* \brief Reset the prefix cache to initial status.
*/
virtual void Reset() = 0;
/*! \brief Return the prefix cache mode. */
virtual PrefixCacheMode Mode() = 0;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<PrefixCacheObj>();
}
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.PrefixCache", PrefixCacheObj, Object);
};
class PrefixCache : public ObjectRef {
public:
/*!
* \brief Initialization of prefix cache.
* \param max_recycling_seqs The maximum number of recycling sequences in prefix cache.
* \param remove_callback The optional callback function to call when removing a sequence.
*/
static PrefixCache CreateRadixPrefixCache(size_t max_recycling_seqs,
PrefixCacheRemoveCallback remove_callback = nullptr);
/*!
* \brief Initialization of no prefix cache.
*/
static PrefixCache CreateNoPrefixCache();
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(PrefixCache, ObjectRef, PrefixCacheObj);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_PREFIX_CACHE_H_
+845
View File
@@ -0,0 +1,845 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/radix_tree.cc
*/
#include "radix_tree.h"
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/runtime/logging.h>
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
TVM_FFI_STATIC_INIT_BLOCK() { PagedRadixTreeObj::RegisterReflection(); }
/*!
* \brief The sequence ID linked list structure in paged radix tree node.
*/
struct SequenceIDNode {
/*! \brief The stored sequence ID. */
int64_t id = 0;
/*! \brief The pointer to the next sequence ID. */
SequenceIDNode* next = nullptr;
};
/*!
* \brief The sequence ID node pool.
*
* The sequence ID node pool allocates a block of sequence ID nodes when pool is full,
* and frees all when destruction, to avoid frequent memory operation.
*/
class SequenceIDNodePool {
public:
/*! \brief The constructor of sequence ID node pool, allocating a new sequence ID node block. */
SequenceIDNodePool() {
NewNodeBlock_();
used_nodes_.clear();
}
/*!
* \brief Get a sequence ID node from pool, and assign the fields.
* If there is no available node, it will allocate a new sequence ID node block.
* \param seq_id The assigned sequence ID of allocated sequence ID node.
* \param node The next sequence ID node pointer of allocated sequence ID node.
* \return The allocated radix page.
*/
SequenceIDNode* Allocate(int64_t seq_id, SequenceIDNode* next) {
if (free_node_indices_.empty()) {
NewNodeBlock_();
TVM_FFI_ICHECK(!free_node_indices_.empty());
}
size_t id = free_node_indices_.back();
free_node_indices_.pop_back();
SequenceIDNode* node = nodes_[id];
used_nodes_[node] = id;
node->id = seq_id;
node->next = next;
return node;
}
/*!
* \brief Free a sequence ID node to pool.
* \param node The sequence ID node to free.
*/
void Free(SequenceIDNode* node) {
TVM_FFI_ICHECK(used_nodes_.find(node) != used_nodes_.end());
free_node_indices_.push_back(used_nodes_[node]);
used_nodes_.erase(node);
}
/*!
* \brief Reset the sequence ID node pool to initial status.
*/
void Reset() {
used_nodes_.clear();
free_node_indices_.reserve(nodes_.size());
for (size_t i = 0; i < nodes_.size(); ++i) {
nodes_[i]->id = 0;
nodes_[i]->next = nullptr;
free_node_indices_[i] = i;
}
}
/*! \brief The destructor of sequence ID node pool, freeing memory for each node. */
~SequenceIDNodePool() {
for (SequenceIDNode* node_block : node_blocks_) {
delete[] node_block;
}
}
private:
/*! \brief The size of each node pool block. */
static constexpr size_t kNodeBlockSize_ = 64;
/*! \brief The raw sequence ID node block pool, each element is a sequence ID node array. */
std::vector<SequenceIDNode*> node_blocks_;
/*! \brief The sequence ID node pool, each element is a sequence ID node pointer. */
std::vector<SequenceIDNode*> nodes_;
/*! \brief The indices of free sequence ID node in node pool. */
std::vector<size_t> free_node_indices_;
/*! \brief The map from used paged sequence ID node to its index in node pool. */
std::unordered_map<SequenceIDNode*, size_t> used_nodes_;
/*! \brief Allocate a new node pool block. */
void NewNodeBlock_() {
size_t node_id_offset = node_blocks_.size() * kNodeBlockSize_;
node_blocks_.push_back(new SequenceIDNode[kNodeBlockSize_]);
nodes_.reserve(nodes_.size() + kNodeBlockSize_);
free_node_indices_.reserve(free_node_indices_.size() + kNodeBlockSize_);
for (size_t i = 0; i < kNodeBlockSize_; ++i) {
nodes_.push_back(&node_blocks_.back()[i]);
free_node_indices_.push_back(i + node_id_offset);
}
}
};
/*!
* \brief The paged radix tree node data structure.
*
* The paged radix tree node is similar to original radix tree node, but with the limited length for
* prefix in page, so that the memory usage in each page is the same and is fixed once allocated.
* Since the page only consists of pointers and int tokens, the page memory layout is int array
* indeed. The lower offset is the pointers and page information, while the higher offset is the
* stored prefix tokens.
*
* And since the vocabulary size may be very large, the paged Radix tree is represented
* as left-child, right-sibling binary tree.
*
* Also, due to possible pop/push front/back tokens in page, the page is designed as circular
* buffer, to make full use of each page.
*
* Each page records the sequence exactly ends with the prefix tokens stored in page. In other word,
* all sequences locate in the boundary of each page, or the end of each page.
*/
struct RadixPage {
/*! \brief The parent page. */
RadixPage* parent;
/*! \brief The first child page. */
RadixPage* first_child;
/*! \brief The sibling page sharing the same parent page. */
RadixPage* next_sibling;
/*! \brief The head of sequence ID linked list. */
SequenceIDNode* seq_ids;
/*! \brief The capacity of maximum stored prefix tokens. */
size_t capacity;
/*! \brief The start offset of stored prefix tokens. The legal value is of [0, capacity). */
size_t offset;
/*! \brief The length of stored prefix tokens. The legal value is of [0, capacity). */
size_t length;
/*! \brief The offset of first prefix token in memory layout. */
static constexpr int kDataOffset = (sizeof(RadixPage*) * 3 + sizeof(SequenceIDNode*) +
sizeof(size_t) * 3 + sizeof(int32_t) - 1) /
sizeof(int32_t);
/*!
* \brief Overload operator [] to get the prefix tokens by index as simple int array.
* \param i The prefix token index.
* \return The value of i-th prefix token.
*/
int32_t& operator[](size_t i) {
return reinterpret_cast<int32_t*>(this)[kDataOffset + (i + offset) % capacity];
}
/*!
* \brief Extend or push back a suffix tokens in page.
* \param suffix The suffix tokens array.
* \param suffix_length The suffix length to extend.
* \throw Error if suffix length is larger than current vacant space.
*/
void Extend(const int32_t* suffix, size_t suffix_length) {
TVM_FFI_ICHECK_LE(suffix_length + length, capacity);
for (int i = 0; i < suffix_length; ++i) {
(*this)[i + length] = suffix[i];
}
length += suffix_length;
}
/*!
* \brief Add a sequence ID in page.
* \param pool The sequence ID node pool to allocate new node.
* \param id The sequence ID to add.
*/
void AddSequence(SequenceIDNodePool* pool, int64_t id) { seq_ids = pool->Allocate(id, seq_ids); }
/*!
* \brief Pop a sequence ID in page.
* \param pool The sequence ID node pool to free popped node.
* \param id The sequence ID to pop.
* \throw Error if no such sequence ID in page.
*/
void PopSequence(SequenceIDNodePool* pool, int64_t id) {
if (seq_ids->id == id) {
// If the popped sequence ID is the first node in linked list,
// directly skip from head and free it.
SequenceIDNode* next = seq_ids->next;
pool->Free(seq_ids);
seq_ids = next;
} else {
// If the popped sequence ID is not the first node in linked list,
// skip it from previous node and free it.
SequenceIDNode* last = seq_ids;
SequenceIDNode* cur = seq_ids->next;
while (cur) {
if (cur->id == id) {
last->next = cur->next;
pool->Free(cur);
return;
}
last = cur;
cur = cur->next;
}
LOG(FATAL) << "Sequence ID = " << id << " not found.";
}
}
/*!
* \brief Get all sequence ID in page.
* \return The std::vector of sequence ID in page.
*/
std::vector<int64_t> GetLocalSequence() {
std::vector<int64_t> output;
for (SequenceIDNode* node = seq_ids; node; node = node->next) {
output.push_back(node->id);
}
return output;
}
/*!
* \brief Get any sequence ID in current page or child pages.
* Since there is always a sequence in leaf pages, it only check first child if no sequence ID in
* current page.
* \return The any sequence ID in current page or child pages.
*/
int32_t FindAnyChildSequence() {
if (seq_ids) return seq_ids->id;
return first_child->FindAnyChildSequence();
}
/*!
* \brief Get all sequence ID in current page and child pages, using Iterate method with lambda
* expression as callback to avoid frequently memory allocation of std::vector.
* \return The std::vector of all sequence ID in current page and child pages.
*/
std::vector<int64_t> FindAllChildSequence() {
std::vector<int64_t> output = GetLocalSequence();
if (first_child) {
first_child->Iterate([&output](const RadixPage* page) {
for (SequenceIDNode* node = page->seq_ids; node; node = node->next) {
output.push_back(node->id);
}
});
}
return output;
}
/*!
* \brief The iteration method for tree or sub-tree traverse.
* \param f The callback function to invoke at each radix page visited.
*/
template <class CallbackFunc>
void Iterate(CallbackFunc f) {
f(this);
if (next_sibling) next_sibling->Iterate(f);
if (first_child) first_child->Iterate(f);
}
/*!
* \brief Get the last sibling of current page.
* \return The page whose next_sibling is current page, or nullptr if current is the first_child
* of its parent page.
*/
RadixPage* GetLastSibling() {
if (parent == nullptr) return nullptr;
if (parent->first_child == this) return nullptr;
for (RadixPage* child = parent->first_child; child; child = child->next_sibling) {
if (child->next_sibling == this) return child;
}
return nullptr;
}
/*!
* \brief Find the child indexed by first token.
* \return The child page started with first token, or nullptr if no such child page.
*/
RadixPage* FindChild(int64_t first_token) {
int32_t casted = first_token;
// Iterate all child radix pages, as the child radix pages are stored unorderly.
for (RadixPage* child = first_child; child; child = child->next_sibling) {
if ((*child)[0] == casted) return child;
}
return nullptr;
}
/*! \brief Insert a new child page. */
void InsertChild(RadixPage* child) {
child->parent = this;
child->next_sibling = first_child;
first_child = child;
}
/*!
* \brief Remove a child page.
* \throw Error if page to be removed is not child page.
*/
void RemoveChild(RadixPage* child) {
TVM_FFI_ICHECK(child->parent == this);
if (first_child == child) {
first_child = child->next_sibling;
} else {
child->GetLastSibling()->next_sibling = child->next_sibling;
}
}
/*!
* \brief Check current page is mergable with its child page.
* The page is mergable if and only if
* 1. No sequence ID in current page, as sequence ID is not allowed to exist within page.
* 2. The current page has child page.
* 3. The current page has only one child page.
* 4. The current page prefix and the child page prefix can be concatenated into one page.
* \return True if current page is mergable, or false.
*/
bool Mergeable() {
if (seq_ids) return false;
if (!first_child) return false;
if (first_child->next_sibling) return false;
if (length + first_child->length > capacity) return false;
return true;
}
/*!
* \brief Match the given prefix within page.
* \param prefix The prefix token array.
* \param prefix_length The length of prefix token array.
* \return The matched prefix offset within page, or the first mismatched token position. The
* possible return value is [0, page->length], where page->length means the page is completely the
* prefix of given prefix.
*/
size_t MatchPrefix(const int32_t* prefix, size_t prefix_length) {
size_t n = std::min(length, prefix_length);
for (int i = 0; i < n; ++i) {
if ((*this)[i] != prefix[i]) return i;
}
return n;
}
};
/*!
* \brief The paged radix tree page pool.
*
* The paged radix tree page pool allocates a block of radix tree pages when pool is full,
* and frees all when destruction, to avoid frequent memory operation.
*/
class RadixPagePool {
public:
/*! \brief The constructor of paged radix tree page pool, allocating memory for each page. */
RadixPagePool() {
NewPageBlock_();
used_pages_.clear();
}
/*!
* \brief Get a radix page from pool.
* If there is no available page, it will allocate a new radix page block.
* \return The allocated radix page.
*/
RadixPage* Allocate() {
if (free_page_indices_.empty()) {
NewPageBlock_();
TVM_FFI_ICHECK(!free_page_indices_.empty());
}
int id = free_page_indices_.back();
free_page_indices_.pop_back();
RadixPage* page = pages_[id];
used_pages_[page] = id;
page->parent = page->first_child = page->next_sibling = nullptr;
page->capacity = kPageCapacity_;
page->offset = page->length = 0;
page->seq_ids = nullptr;
return page;
}
/*!
* \brief Free a radix page to pool.
* \param page The radix page to free.
*/
void Free(RadixPage* page) {
TVM_FFI_ICHECK_EQ(page->seq_ids, nullptr);
TVM_FFI_ICHECK(used_pages_.find(page) != used_pages_.end());
free_page_indices_.push_back(used_pages_[page]);
TVM_FFI_ICHECK(used_pages_.erase(page));
}
/*!
* \brief Get the token capacity of free pages.
* \return The the token capacity of free pages.
*/
size_t FreeCapacity() { return free_page_indices_.size() * kPageCapacity_; }
/*!
* \brief Reset the paged radix tree page pool to initial status.
*/
void Reset() {
used_pages_.clear();
free_page_indices_.reserve(pages_.size());
for (int i = 0; i < pages_.size(); ++i) {
pages_[i]->parent = pages_[i]->first_child = pages_[i]->next_sibling = nullptr;
pages_[i]->capacity = kPageCapacity_;
pages_[i]->offset = pages_[i]->length = 0;
pages_[i]->seq_ids = nullptr;
free_page_indices_[i] = i;
}
}
/*! \brief The destructor of paged radix tree page pool, freeing memory for each page. */
~RadixPagePool() {
for (int32_t* page_block : page_blocks_) {
delete[] page_block;
}
}
private:
/*! \brief The size of each page pool block. */
static constexpr size_t kPageBlockSize_ = 64;
/*! \brief The page capacity of each paged radix tree page. */
static constexpr size_t kPageCapacity_ = 64;
/*! \brief The page size of each paged radix tree page. */
static constexpr size_t kPageSize_ = kPageCapacity_ + RadixPage::kDataOffset;
/*! \brief The raw paged radix tree page block pool,
each element is a raw paged radix tree page array. */
std::vector<int32_t*> page_blocks_;
/*! \brief The paged radix tree page pool,
each element is a raw paged radix tree page pointer. */
std::vector<RadixPage*> pages_;
/*! \brief The indices of free paged radix page in page pool. */
std::vector<size_t> free_page_indices_;
/*! \brief The map from used paged radix tree page to its index in page pool. */
std::unordered_map<RadixPage*, size_t> used_pages_;
/*! \brief Allocate a new page pool block. */
void NewPageBlock_() {
size_t page_id_offset = page_blocks_.size() * kPageBlockSize_;
page_blocks_.push_back(new int32_t[kPageBlockSize_ * kPageSize_]);
pages_.reserve(pages_.size() + kPageBlockSize_);
free_page_indices_.reserve(free_page_indices_.size() + kPageBlockSize_);
for (size_t i = 0; i < kPageBlockSize_; ++i) {
pages_.push_back(reinterpret_cast<RadixPage*>(page_blocks_.back() + i * kPageSize_));
free_page_indices_.push_back(i + page_id_offset);
}
}
};
// PagedRadixTree
/*!
* \brief The paged radix tree data structure.
*/
class PagedRadixTreeImpl : public PagedRadixTreeObj {
public:
/*! \brief The map from sequence to paged radix tree node it is stored. */
std::unordered_map<int32_t, RadixPage*> seq2page;
/*! \brief The sequence ID node pool. */
SequenceIDNodePool* seq_id_node_pool = nullptr;
/*! \brief The radix page pool. */
RadixPagePool* radix_page_pool = nullptr;
/*! \brief The root page of paged radix tree. */
RadixPage* root = nullptr;
explicit PagedRadixTreeImpl() {
seq_id_node_pool = new SequenceIDNodePool();
radix_page_pool = new RadixPagePool();
root = reinterpret_cast<RadixPage*>(new int32_t[RadixPage::kDataOffset]);
root->parent = root->first_child = root->next_sibling = nullptr;
root->offset = root->length = root->capacity = 0;
root->seq_ids = nullptr;
}
/*!
* \brief Check if a sequence exists.
* \param seq_id The sequence ID for index.
* \return The sequence existence.
* \throw Error if sequence ID is not valid.
*/
bool HasSequence(int64_t seq_id) { return seq2page.find(seq_id) != seq2page.end(); }
/*!
* \brief Get a sequence's all tokens.
* \param seq_id The sequence ID for index.
* \return The sequence tokens.
* \throw Error if sequence ID is not valid.
*/
Shape GetSequence(int64_t seq_id) {
TVM_FFI_ICHECK(seq2page.find(seq_id) != seq2page.end());
size_t length = GetSequenceLength(seq_id);
std::vector<int64_t> output(length);
size_t offset = length;
for (RadixPage* page = seq2page[seq_id]; page; page = page->parent) {
offset -= page->length;
for (int i = 0; i < page->length; ++i) {
output[offset + i] = (*page)[i];
}
}
return Shape(output);
}
/*!
* \brief Get all sequences with longest common prefix with give prefix tokens.
* \param tokens The prefix tokens for reference.
* \return The pair of matched prefix length and the array of matched sequences indices.
*/
std::pair<size_t, std::vector<int64_t>> MatchPrefix(const std::vector<int32_t>& tokens) {
const int32_t* prefix = tokens.data();
size_t length = tokens.size();
auto [page, offset, in_page_offset] = MatchSequence(root, prefix, length);
if (!offset) return std::make_pair(0, std::vector<int64_t>());
return std::make_pair(offset, page->FindAllChildSequence());
}
/*!
* \brief Get a sequence's length.
* \param seq_id The sequence ID for index.
* \return The sequence length.
* \throw Error if sequence ID is not valid.
*/
size_t GetSequenceLength(int64_t seq_id) {
TVM_FFI_ICHECK(seq2page.find(seq_id) != seq2page.end());
size_t length = 0;
for (RadixPage* page = seq2page[seq_id]; page; page = page->parent) {
length += page->length;
}
return length;
}
/*!
* \brief Fork a sequence from parent sequence at given position.
* \param seq_id The new sequence ID.
* \param parent_seq_id The parent sequence ID to fork from.
* \param forked_offset The position of parent sequence to fork at.
* The valid value is [1, length of forked sequence]. If the position equals the length of forked
* sequence, the new sequence will copy the entire forked sequence.
* \throw Error if sequence ID or
* forked postion is not valid.
*/
void ForkSequence(int64_t seq_id, int64_t parent_seq_id, size_t forked_offset) {
TVM_FFI_ICHECK(seq2page.find(seq_id) == seq2page.end());
TVM_FFI_ICHECK(seq2page.find(parent_seq_id) != seq2page.end());
TVM_FFI_ICHECK_GT(forked_offset, 0);
size_t length = GetSequenceLength(parent_seq_id);
TVM_FFI_ICHECK_LE(forked_offset, length);
for (RadixPage* page = seq2page[parent_seq_id]; page; page = page->parent) {
if (forked_offset > length - page->length) {
if (forked_offset < length) {
// Split radix page if forked position is within page
page = SplitPage(page, forked_offset + page->length - length);
}
page->AddSequence(seq_id_node_pool, seq_id);
seq2page[seq_id] = page;
return;
}
length -= page->length;
}
}
/*!
* \brief Add an empty sequence at root.
* \param seq_id The new sequence ID.
* \throw Error if sequence ID is not valid.
*/
void AddSequence(int64_t seq_id) {
TVM_FFI_ICHECK(seq2page.find(seq_id) == seq2page.end())
<< "Sequence ID = " << seq_id << " has been added.";
root->AddSequence(seq_id_node_pool, seq_id);
seq2page[seq_id] = root;
}
/*!
* \brief Extend a sequence with given tokens.
* \param seq_id The sequence ID for index.
* \param tokens The given tokens to extend.
* \throw Error if sequence ID is not valid.
*/
void ExtendSequence(int64_t seq_id, const std::vector<int32_t>& tokens) {
TVM_FFI_ICHECK(seq2page.find(seq_id) != seq2page.end());
const int32_t* suffix = tokens.data();
size_t length = tokens.size();
RadixPage* original_page = seq2page[seq_id];
original_page->PopSequence(seq_id_node_pool, seq_id);
auto [page, offset, in_page_offset] = MatchSequence(original_page, suffix, length);
if (in_page_offset < page->length) {
// Split page if extended sequence mismatches within page
page = SplitPage(page, in_page_offset);
}
if (offset < length && !page->seq_ids && !page->first_child && page->capacity > page->length) {
// Extend in the existing leaf page first if possible.
size_t suffix_length = std::min(page->capacity - page->length, length - offset);
page->Extend(suffix + offset, suffix_length);
offset += suffix_length;
}
while (offset < length) {
// Allocate new radix page and extend tokens
RadixPage* new_page = radix_page_pool->Allocate();
page->InsertChild(new_page);
page = new_page;
size_t suffix_length = std::min(page->capacity - page->length, length - offset);
page->Extend(suffix + offset, suffix_length);
offset += suffix_length;
}
page->AddSequence(seq_id_node_pool, seq_id);
seq2page[seq_id] = page;
if (original_page->Mergeable()) {
// The original page may be mergeable, as the sequence ID changes
MergePage(original_page);
}
}
/*!
* \brief Roll back a sequence by number of tokens.
* \param seq_id The sequence ID for index.
* \param num_tokens The number of tokens to be rolled back.
* \throw Error if sequence ID is not valid.
*/
void RollBackSequence(int64_t seq_id, size_t num_tokens) {
size_t length = GetSequenceLength(seq_id);
TVM_FFI_ICHECK_GT(num_tokens, 0);
TVM_FFI_ICHECK_LE(num_tokens, length);
if (num_tokens == length) {
// If rolling back whole sequence, just remove the sequence and add it again equivalently.
RemoveSequence(seq_id);
AddSequence(seq_id);
return;
}
RadixPage* page = seq2page[seq_id];
// Remove the sequence temporarily, but keeping the data and starting rolling back.
page->PopSequence(seq_id_node_pool, seq_id);
seq2page.erase(seq_id);
while (page->length <= num_tokens) {
// Roll back entire page
num_tokens -= page->length;
RadixPage* parent = page->parent;
if (page->seq_ids == nullptr && page->first_child == nullptr) {
// The leaf page is removable
parent->RemoveChild(page);
radix_page_pool->Free(page);
}
page = parent;
}
if (page->seq_ids == nullptr && page->first_child == nullptr) {
// The page is leaf page, directly roll back in page length
page->length -= num_tokens;
// Update the mapping from sequence to page
page->AddSequence(seq_id_node_pool, seq_id);
seq2page[seq_id] = page;
return;
}
// Split page for rolled back sequence
if (num_tokens) {
page = SplitPage(page, page->length - num_tokens);
}
// Update the mapping from sequence to page
page->AddSequence(seq_id_node_pool, seq_id);
seq2page[seq_id] = page;
}
/*!
* \brief Remove a sequence.
* \param seq_id The sequence ID to remove.
* \throw Error if sequence ID is not valid.
*/
void RemoveSequence(int64_t seq_id) {
RadixPage* page = seq2page[seq_id];
page->PopSequence(seq_id_node_pool, seq_id);
seq2page.erase(seq_id);
while (page->parent && !page->seq_ids && !page->first_child) {
RadixPage* parent = page->parent;
parent->RemoveChild(page);
radix_page_pool->Free(page);
page = parent;
}
if (page && page->Mergeable()) {
// The remaining page may be mergeable, as the sequence ID changes
MergePage(page);
}
}
/*!
* \brief Get the remaining token capacity of the paged radix tree.
* \return The the remaining token capacity of the paged radix tree.
*/
size_t FreeCapacity() { return radix_page_pool->FreeCapacity(); }
void Reset() {
radix_page_pool->Reset();
seq_id_node_pool->Reset();
seq2page.clear();
root->parent = root->first_child = root->next_sibling = nullptr;
root->offset = root->length = root->capacity = 0;
root->seq_ids = nullptr;
}
/*! \brief The destructor to free root page. */
~PagedRadixTreeImpl() {
delete[] reinterpret_cast<int32_t*>(root);
delete seq_id_node_pool;
delete radix_page_pool;
}
private:
/*!
* \brief Merge a radix tree page with its child radix tree page, to save radix tree page.
* e.g. MergePage([1, 2, _, _, _] -> [3, 4, 5, _, _]) = [1, 2, 3, 4, 5].
* And the page to be merged should be page->Mergeable().
* \param page The parent radix tree page.
*/
void MergePage(RadixPage* page) {
TVM_FFI_ICHECK(page->Mergeable());
RadixPage* child = page->first_child;
for (int i = 0; i < child->length; ++i) {
(*page)[i + page->length] = (*child)[i];
}
page->length += child->length;
page->first_child = child->first_child;
for (RadixPage* p = child->first_child; p; p = p->next_sibling) {
p->parent = page;
}
page->seq_ids = child->seq_ids;
std::vector<int64_t> seq_ids = page->GetLocalSequence();
for (int64_t id : seq_ids) seq2page[id] = page;
child->seq_ids = nullptr;
radix_page_pool->Free(child);
}
/*!
* \brief Split a radix tree page at given position, to accept new sequence.
* e.g. SplitPage([1, 2, 3, 4, 5], 2) = [1, 2, _, _, _] -> [3, 4, 5, _, _].
* \param page The radix tree page to split.
* \param offset The position to split the radix tree page.
* \return The splitted radix tree page. It can be different from the input radix tree page, as
* there may be implicit radix tree page merge.
*/
RadixPage* SplitPage(RadixPage* page, size_t offset) {
TVM_FFI_ICHECK_LT(offset, page->length);
RadixPage* child = radix_page_pool->Allocate();
child->parent = page;
child->first_child = page->first_child;
for (RadixPage* p = page->first_child; p; p = p->next_sibling) {
p->parent = child;
}
page->first_child = child;
for (int i = offset; i < page->length; ++i) {
(*child)[i - offset] = (*page)[i];
}
child->length = page->length - offset;
page->length = offset;
child->seq_ids = page->seq_ids;
std::vector<int64_t> seq_ids = page->GetLocalSequence();
for (int64_t id : seq_ids) seq2page[id] = child;
page->seq_ids = nullptr;
if (child->Mergeable()) {
// The child page may be mergeable
MergePage(child);
}
if (page->parent && page->parent->Mergeable()) {
// The parent page may be mergeable
page = page->parent;
MergePage(page);
}
return page;
}
/*!
* \brief Match with given token from a radix tree page, stopping at first mismatch.
* \param page The radix tree page to start matching.
* \param tokens The given tokens to match.
* \param length The length of given tokens.
*/
std::tuple<RadixPage*, size_t, size_t> MatchSequence(RadixPage* page, const int32_t* tokens,
size_t length) {
size_t offset = 0;
while (offset < length) {
if (RadixPage* child = page->FindChild(tokens[offset])) {
// If child page starts with offset-th token, common prefix at least ends with child page
size_t matched_offset = child->MatchPrefix(tokens + offset, length - offset);
offset += matched_offset;
if (matched_offset < child->length) {
// Common prefix ends within child page
return std::make_tuple(child, offset, matched_offset);
}
page = child;
} else {
// No child page starts with offset-th token, common prefix ends with current page
return std::make_tuple(page, offset, page->length);
}
}
return std::make_tuple(page, length, page->length);
}
};
PagedRadixTree PagedRadixTree::Create() {
return PagedRadixTree(tvm::ffi::make_object<PagedRadixTreeImpl>());
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("mlc.serve.PagedRadixTree", []() { return PagedRadixTree::Create(); })
.def("mlc.serve.PagedRadixTreeMatchPrefix",
[](PagedRadixTree paged_radix_tree, Shape tokens) {
std::vector<int32_t> token_ids{tokens.begin(), tokens.end()};
auto [offset, seq_ids] = paged_radix_tree->MatchPrefix(token_ids);
seq_ids.insert(seq_ids.begin(), offset);
return Shape(seq_ids);
})
.def("mlc.serve.PagedRadixTreeExtendSequence",
[](PagedRadixTree paged_radix_tree, int64_t seq_id, Shape tokens) {
std::vector<int32_t> token_ids{tokens.begin(), tokens.end()};
paged_radix_tree->ExtendSequence(seq_id, std::move(token_ids));
})
.def("mlc.serve.PagedRadixTreeRollBackSequence",
[](PagedRadixTree paged_radix_tree, int64_t seq_id, int64_t num_tokens) {
paged_radix_tree->RollBackSequence(seq_id, num_tokens);
})
.def("mlc.serve.PagedRadixTreeForkSequence",
[](PagedRadixTree paged_radix_tree, int64_t seq_id, int64_t parent_seq_id,
uint64_t forked_offset) {
paged_radix_tree->ForkSequence(seq_id, parent_seq_id, forked_offset);
})
.def_method("mlc.serve.PagedRadixTreeHasSequence", &PagedRadixTreeObj::HasSequence)
.def_method("mlc.serve.PagedRadixTreeAddSequence", &PagedRadixTreeObj::AddSequence)
.def_method("mlc.serve.PagedRadixTreeRemoveSequence", &PagedRadixTreeObj::RemoveSequence)
.def_method("mlc.serve.PagedRadixTreeGetSequence", &PagedRadixTreeObj::GetSequence)
.def("mlc.serve.PagedRadixTreeGetSequenceLength",
[](PagedRadixTree paged_radix_tree, int64_t seq_id) {
return static_cast<int64_t>(paged_radix_tree->GetSequenceLength(seq_id));
})
.def("mlc.serve.PagedRadixTreeFreeCapacity", [](PagedRadixTree paged_radix_tree) {
return static_cast<int64_t>(paged_radix_tree->FreeCapacity());
});
}
} // namespace serve
} // namespace llm
} // namespace mlc
+134
View File
@@ -0,0 +1,134 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/radix_tree.h
*/
#ifndef MLC_LLM_SERVE_RADIX_TREE_H_
#define MLC_LLM_SERVE_RADIX_TREE_H_
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/reflection/registry.h>
#include <unordered_map>
#include <unordered_set>
namespace mlc {
namespace llm {
namespace serve {
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
using tvm::ffi::Shape;
/*!
* \brief The paged radix tree data structure.
*/
class PagedRadixTreeObj : public Object {
public:
/*!
* \brief Check if a sequence exists.
* \param seq_id The sequence ID for index.
* \return The sequence existence.
* \throw Error if sequence ID is not valid.
*/
virtual bool HasSequence(int64_t seq_id) = 0;
/*!
* \brief Get a sequence's all tokens.
* \param seq_id The sequence ID for index.
* \return The sequence tokens.
* \throw Error if sequence ID is not valid.
*/
virtual Shape GetSequence(int64_t seq_id) = 0;
/*!
* \brief Get all sequences with longest common prefix with give prefix tokens.
* \param tokens The prefix tokens for reference.
* \return The pair of matched prefix length and the array of matched sequences indices.
*/
virtual std::pair<size_t, std::vector<int64_t>> MatchPrefix(
const std::vector<int32_t>& tokens) = 0;
/*!
* \brief Get a sequence's length.
* \param seq_id The sequence ID for index.
* \return The sequence length.
* \throw Error if sequence ID is not valid.
*/
virtual size_t GetSequenceLength(int64_t seq_id) = 0;
/*!
* \brief Fork a sequence from parent sequence at given position.
* \param seq_id The new sequence ID.
* \param parent_seq_id The parent sequence ID to fork from.
* \param forked_offset The position of parent sequence to fork at.
* The valid value is [1, length of forked sequence]. If the position equals the length of forked
* sequence, the new sequence will copy the entire forked sequence.
* \throw Error if sequence ID or
* forked postion is not valid.
*/
virtual void ForkSequence(int64_t seq_id, int64_t parent_seq_id, size_t forked_offset) = 0;
/*!
* \brief Add an empty sequence at root.
* \param seq_id The new sequence ID.
* \throw Error if sequence ID is not valid.
*/
virtual void AddSequence(int64_t seq_id) = 0;
/*!
* \brief Extend a sequence with given tokens.
* \param seq_id The sequence ID for index.
* \param tokens The given tokens to extend.
* \throw Error if sequence ID is not valid.
*/
virtual void ExtendSequence(int64_t seq_id, const std::vector<int32_t>& tokens) = 0;
/*!
* \brief Roll back a sequence by number of tokens.
* \param seq_id The sequence ID for index.
* \param num_tokens The number of tokens to be rolled back.
* \throw Error if sequence ID is not valid.
*/
virtual void RollBackSequence(int64_t seq_id, size_t num_tokens) = 0;
/*!
* \brief Remove a sequence.
* \param seq_id The sequence ID to remove.
* \throw Error if sequence ID is not valid.
*/
virtual void RemoveSequence(int64_t seq_id) = 0;
/*!
* \brief Get the remaining token capacity of the paged radix tree.
* \return The the remaining token capacity of the paged radix tree.
*/
virtual size_t FreeCapacity() = 0;
/*!
* \brief Reset the paged radix tree to initial status.
*/
virtual void Reset() = 0;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<PagedRadixTreeObj>();
}
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.PagedRadixTree", PagedRadixTreeObj, Object);
};
class PagedRadixTree : public ObjectRef {
public:
/*!
* \brief Construct a paged radix tree.
* \return The constructed paged radix tree. */
static PagedRadixTree Create();
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(PagedRadixTree, ObjectRef, PagedRadixTreeObj);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_RADIX_TREE_H_
+82
View File
@@ -0,0 +1,82 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/request.cc
*/
#include "request.h"
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include "data.h"
namespace mlc {
namespace llm {
namespace serve {
/****************** Request ******************/
TVM_FFI_STATIC_INIT_BLOCK() { RequestNode::RegisterReflection(); }
Request::Request(String id, Array<Data> inputs, GenerationConfig generation_cfg) {
if (generation_cfg->debug_config.special_request == SpecialRequestKind::kNone) {
TVM_FFI_ICHECK(!inputs.empty()) << "No input data is given.";
}
// Compute the total input length, or fall back to "-1" which means
// unknown due to the existence of untokenized data.
int prompt_tokens = 0;
for (Data input : inputs) {
if (const auto* token_data = input.as<TokenDataNode>()) {
prompt_tokens += token_data->token_ids.size();
} else if (const auto* image_data = input.as<ImageDataNode>()) {
prompt_tokens += image_data->GetLength();
} else {
prompt_tokens = -1;
break;
}
}
ObjectPtr<RequestNode> n = tvm::ffi::make_object<RequestNode>();
n->id = std::move(id);
n->inputs = std::move(inputs);
n->prompt_tokens = prompt_tokens;
n->generation_cfg = std::move(generation_cfg);
data_ = std::move(n);
}
Request Request::FromUntokenized(const Request& request, const Tokenizer& tokenizer) {
bool has_untokenized_input = false;
Array<Data> inputs;
inputs.reserve(request->inputs.size());
// Tokenize all text inputs.
for (Data input : request->inputs) {
if (const auto* text_data = input.as<TextDataNode>()) {
has_untokenized_input = true;
std::vector<int> token_ids = tokenizer->Encode(text_data->text);
inputs.push_back(TokenData(token_ids));
} else {
inputs.push_back(input);
}
}
// If there is no untokenized input, we don't need to create a new request.
if (!has_untokenized_input) {
TVM_FFI_ICHECK_NE(request->prompt_tokens, -1);
return request;
} else {
return Request(request->id, std::move(inputs), request->generation_cfg);
}
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("mlc.serve.RequestGetInputs", [](Request request) { return request->inputs; })
.def("mlc.serve.RequestGetGenerationConfigJSON", [](Request request) {
return tvm::ffi::json::Stringify(request->generation_cfg->AsJSON());
});
}
} // namespace serve
} // namespace llm
} // namespace mlc
+92
View File
@@ -0,0 +1,92 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/request.h
* \brief Implementation of llm chat.
*/
#ifndef MLC_LLM_SERVE_REQUEST_H_
#define MLC_LLM_SERVE_REQUEST_H_
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
#include "../tokenizers/tokenizers.h"
#include "config.h"
#include "data.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
/****************** Request ******************/
/*!
* \brief The user submitted text-generation request, which contains
* a unique request id, a list of multi-modal inputs, a set of
* generation configuration parameters.
* \note Request is immutable and can be re-dispatched to another
* node and restart the request handling on the new one.
*/
class RequestNode : public Object {
public:
/*!
* \brief The unique identifier of the request.
* Different requests should have different ids.
*/
String id;
/*!
* \brief The user inputs of a request. Input may have multi-modality.
* \sa data.h
*/
Array<Data> inputs;
/*!
* \brief The equivalent input sequence length of the request.
* "-1" means the input length is unknown due to the existence
* of untokenized text data.
*/
int prompt_tokens = -1;
/*!
* \brief The sampling configuration which may contain temperature,
* top_p, repetition_penalty, max_gen_len, etc.
*/
GenerationConfig generation_cfg;
/*! \brief Backward reference to the request state. */
Object* rstate = nullptr;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<RequestNode>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.Request", RequestNode, Object);
};
class Request : public ObjectRef {
public:
explicit Request(String id, Array<Data> inputs, GenerationConfig generation_cfg);
/*!
* \brief Return a request object with all text data tokenized,
* and the request ID kept the same as the input one.
* \param request The request to be tokenized.
* \param tokenizer The tokenizer to tokenize the input data of the given request.
* \return The request object whose data are tokenized.
*/
static Request FromUntokenized(const Request& request, const Tokenizer& tokenizer);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(Request, ObjectRef, RequestNode);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_REQUEST_H_
+298
View File
@@ -0,0 +1,298 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/request_state.cc
*/
#include "request_state.h"
#include <unordered_set>
namespace mlc {
namespace llm {
namespace serve {
TVM_FFI_STATIC_INIT_BLOCK() {
RequestModelStateNode::RegisterReflection();
RequestStateEntryNode::RegisterReflection();
RequestStateNode::RegisterReflection();
}
/****************** RequestModelState ******************/
RequestModelState::RequestModelState(
Request request, int model_id, int64_t internal_id, Array<Data> inputs,
const std::optional<xgrammar::CompiledGrammar>& compiled_grammar) {
ObjectPtr<RequestModelStateNode> n = tvm::ffi::make_object<RequestModelStateNode>();
n->model_id = model_id;
n->internal_id = internal_id;
n->inputs = std::move(inputs);
if (compiled_grammar.has_value()) {
// TODO(yixin): set rollback limit to a configurable value.
n->grammar_matcher =
xgrammar::GrammarMatcher(compiled_grammar.value(), std::nullopt, false, std::nullopt, 10);
}
n->request = std::move(request);
data_ = std::move(n);
}
int RequestModelStateNode::GetInputLength() const {
int total_length = 0;
for (Data input : inputs) {
total_length += input->GetLength();
}
return total_length;
}
bool RequestModelStateNode::RequireNextTokenBitmask() { return grammar_matcher.has_value(); }
void RequestModelStateNode::GetNextTokenBitmask(DLTensor* bitmask) {
TVM_FFI_ICHECK(grammar_matcher.has_value());
grammar_matcher->GetNextTokenBitmask(bitmask);
}
void RequestModelStateNode::CommitToken(SampleResult sampled_token) {
committed_tokens.push_back(std::move(sampled_token));
appeared_token_ids[sampled_token.GetTokenId()] += 1;
// There will be one more token that will be processed in the next decoding.
++num_tokens_for_next_decode;
// Update the grammar matcher state if it exists.
if (grammar_matcher) {
bool accepted = grammar_matcher->AcceptToken(sampled_token.GetTokenId());
TVM_FFI_ICHECK(accepted) << "Token id " << sampled_token.GetTokenId()
<< " is not accepted by the grammar state matcher.";
}
}
void RequestModelStateNode::RollbackTokens(int count) {
TVM_FFI_ICHECK(count <= static_cast<int>(committed_tokens.size()));
for (int i = 0; i < count; ++i) {
auto it = appeared_token_ids.find(committed_tokens.back().GetTokenId());
TVM_FFI_ICHECK(it != appeared_token_ids.end());
if (--it->second == 0) {
appeared_token_ids.erase(it);
}
committed_tokens.pop_back();
if (grammar_matcher) {
grammar_matcher->Rollback(1);
}
}
}
void RequestModelStateNode::AddDraftToken(SampleResult sampled_token, int draft_token_slot,
int64_t parent_idx) {
draft_output_tokens.push_back(std::move(sampled_token));
draft_token_slots.push_back(draft_token_slot);
draft_token_parent_idx.push_back(parent_idx);
draft_token_first_child_idx.push_back(-1);
if (parent_idx != -1) {
if (draft_token_first_child_idx[parent_idx] == -1) {
draft_token_first_child_idx[parent_idx] = static_cast<int>(draft_output_tokens.size()) - 1;
}
}
}
void RequestModelStateNode::RemoveAllDraftTokens(std::vector<int>* removed_draft_token_slots) {
if (removed_draft_token_slots != nullptr) {
std::unordered_set<int> dedup;
removed_draft_token_slots->clear();
for (auto slot : draft_token_slots) {
bool inserted = dedup.insert(slot).second;
if (inserted) {
removed_draft_token_slots->push_back(slot);
}
}
}
draft_token_slots.clear();
draft_token_parent_idx.clear();
draft_token_first_child_idx.clear();
draft_output_tokens.clear();
}
/****************** RequestActionPostProcWorkspace ******************/
RequestStreamOutput RequestActionPostProcWorkspace::GetStreamOutput() {
for (const RequestStreamOutput& stream_output : stream_outputs) {
if (stream_output->unpacked) {
return stream_output;
}
}
TVM_FFI_ICHECK(!stream_outputs.empty());
int num_response = stream_outputs[0]->group_delta_token_ids.size();
std::vector<std::vector<int64_t>> group_delta_token_ids;
std::vector<std::vector<String>> group_delta_logprob_json_strs;
std::vector<Optional<String>> group_finish_reason;
std::vector<String> group_extra_prefix_string;
group_delta_token_ids.resize(num_response);
group_finish_reason.resize(num_response);
group_extra_prefix_string.resize(num_response);
if (stream_outputs[0]->group_delta_logprob_json_strs.has_value()) {
group_delta_logprob_json_strs.resize(num_response);
}
RequestStreamOutput stream_output(stream_outputs[0]->request_id, std::move(group_delta_token_ids),
stream_outputs[0]->group_delta_logprob_json_strs.has_value()
? std::make_optional(group_delta_logprob_json_strs)
: std::nullopt,
std::move(group_finish_reason),
std::move(group_extra_prefix_string));
stream_outputs.push_back(stream_output);
return stream_output;
}
/****************** RequestStateEntry ******************/
RequestStateEntry::RequestStateEntry(
Request request, int num_models, int64_t internal_id, int rng_seed,
const std::vector<std::string>& token_table,
const std::optional<xgrammar::CompiledGrammar>& compiled_grammar, int parent_idx) {
ObjectPtr<RequestStateEntryNode> n = tvm::ffi::make_object<RequestStateEntryNode>();
Array<RequestModelState> mstates;
Array<Data> inputs;
if (parent_idx == -1) {
inputs = request->inputs;
}
mstates.reserve(num_models);
for (int i = 0; i < num_models; ++i) {
mstates.push_back(RequestModelState(request, i, internal_id, inputs, compiled_grammar));
}
n->status = RequestStateStatus::kPending;
n->rng = RandomGenerator(rng_seed);
n->stop_str_handler = StopStrHandler(!request->generation_cfg->debug_config.ignore_eos
? request->generation_cfg->stop_strs
: Array<String>(),
token_table);
n->request = std::move(request);
n->parent_idx = parent_idx;
n->mstates = std::move(mstates);
n->next_callback_token_pos = 0;
data_ = std::move(n);
}
void RequestStateEntryNode::GetDeltaRequestReturn(const Tokenizer& tokenizer,
int64_t max_single_sequence_length,
RequestStreamOutput* delta_stream_output,
int idx) {
TVM_FFI_ICHECK_NOTNULL(delta_stream_output);
bool needs_logprobs = (*delta_stream_output)->group_delta_logprob_json_strs.has_value();
(*delta_stream_output)->group_delta_token_ids[idx].clear();
if (needs_logprobs) {
(*delta_stream_output)->group_delta_logprob_json_strs.value()[idx].clear();
}
(*delta_stream_output)->group_finish_reason[idx] = std::nullopt;
(*delta_stream_output)->group_extra_prefix_string[idx] = this->extra_prefix_string;
this->extra_prefix_string.clear();
const std::vector<SampleResult>& committed_tokens = this->mstates[0]->committed_tokens;
int num_committed_tokens = committed_tokens.size();
TVM_FFI_ICHECK_LE(this->next_callback_token_pos, num_committed_tokens);
// Case 1. There is no new token ids.
if (this->next_callback_token_pos == num_committed_tokens && extra_prefix_string.empty()) {
return;
}
// Case 2. Any of the stop strings is matched.
TVM_FFI_ICHECK(!stop_str_handler->StopTriggered());
while (next_callback_token_pos < num_committed_tokens) {
stop_str_handler->Put(committed_tokens[next_callback_token_pos].GetTokenId(),
&(*delta_stream_output)->group_delta_token_ids[idx]);
if (needs_logprobs) {
(*delta_stream_output)
->group_delta_logprob_json_strs.value()[idx]
.push_back(committed_tokens[next_callback_token_pos].GetLogProbJSON(
tokenizer, request->generation_cfg->logprobs));
}
++next_callback_token_pos;
if (stop_str_handler->StopTriggered()) {
(*delta_stream_output)->group_finish_reason[idx] = "stop";
break;
}
}
// Case 3. Any of the stop tokens appears in the committed tokens ===> Finished
// `stop_token_ids` includes the stop tokens from conversation template and user-provided tokens.
// This check will be ignored when `ignore_eos` is set for the benchmarking purpose.
if (!request->generation_cfg->debug_config.ignore_eos) {
for (int i = 0; i < static_cast<int>((*delta_stream_output)->group_delta_token_ids[idx].size());
++i) {
if (std::any_of(request->generation_cfg->stop_token_ids.begin(),
request->generation_cfg->stop_token_ids.end(),
[delta_stream_output, idx, i](int32_t token) {
return token == (*delta_stream_output)->group_delta_token_ids[idx][i];
})) {
// Stop token matched. Erase the stop token and all tokens after it.
(*delta_stream_output)->group_finish_reason[idx] = "stop";
while (static_cast<int>((*delta_stream_output)->group_delta_token_ids[idx].size()) > i) {
(*delta_stream_output)->group_delta_token_ids[idx].pop_back();
}
break;
}
}
}
// Case 4. When stop token is not detected (e.g. ignore_eos is set), but the grammar state is
// terminated, stop the generation and pop the last token (used to trigger the termination).
if ((*delta_stream_output)->group_finish_reason[idx] != "stop" &&
this->mstates[0]->grammar_matcher.has_value() &&
this->mstates[0]->grammar_matcher->IsTerminated()) {
(*delta_stream_output)->group_delta_token_ids[idx].pop_back();
(*delta_stream_output)->group_finish_reason[idx] = "stop";
}
if ((*delta_stream_output)->group_finish_reason[idx].has_value()) {
return;
}
// Case 5. Generation reaches the specified max generation length ==> Finished
// `max_tokens` means the generation length is limited by model capacity.
if (request->generation_cfg->max_tokens >= 0 &&
num_committed_tokens >= request->generation_cfg->max_tokens) {
stop_str_handler->Finish(&(*delta_stream_output)->group_delta_token_ids[idx]);
(*delta_stream_output)->group_finish_reason[idx] = "length";
return;
}
// Case 6. Total length of the request reaches the maximum single sequence length ==> Finished
if (request->prompt_tokens + num_committed_tokens >= max_single_sequence_length) {
stop_str_handler->Finish(&(*delta_stream_output)->group_delta_token_ids[idx]);
(*delta_stream_output)->group_finish_reason[idx] = "length";
}
}
/****************** RequestState ******************/
RequestState::RequestState(std::vector<RequestStateEntry> entries, int num_response,
std::chrono::high_resolution_clock::time_point add_time_point) {
TVM_FFI_ICHECK(!entries.empty());
ObjectPtr<RequestStateNode> n = tvm::ffi::make_object<RequestStateNode>();
n->entries = std::move(entries);
n->metrics.prompt_tokens = n->entries[0]->request->prompt_tokens;
n->metrics.add_time_point = add_time_point;
std::vector<std::vector<int64_t>> group_delta_token_ids;
std::vector<std::vector<String>> group_delta_logprob_json_strs;
std::vector<Optional<String>> group_finish_reason;
std::vector<String> group_extra_prefix_string;
group_delta_token_ids.resize(num_response);
group_finish_reason.resize(num_response);
group_extra_prefix_string.resize(num_response);
if (n->entries[0]->request->generation_cfg->logprobs) {
group_delta_logprob_json_strs.resize(num_response);
}
RequestStreamOutput stream_output(n->entries[0]->request->id, std::move(group_delta_token_ids),
n->entries[0]->request->generation_cfg->logprobs
? std::make_optional(group_delta_logprob_json_strs)
: std::nullopt,
std::move(group_finish_reason),
std::move(group_extra_prefix_string));
stream_output->unpacked = true;
n->postproc_states.stream_outputs = {std::move(stream_output)};
data_ = std::move(n);
}
} // namespace serve
} // namespace llm
} // namespace mlc
+318
View File
@@ -0,0 +1,318 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/request_state.h
* \brief The data structure maintaining the generation states of user requests.
*/
#ifndef MLC_LLM_SERVE_REQUEST_STATE_H_
#define MLC_LLM_SERVE_REQUEST_STATE_H_
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/object.h>
#include <tvm/runtime/tensor.h>
#include <xgrammar/xgrammar.h>
#include <optional>
#include "../support/random.h"
#include "../tokenizers/streamer.h"
#include "config.h"
#include "metrics.h"
#include "request.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
/*!
* \brief The state of a request with regard to some single model.
* \details In MLC LLM, the serving engine may leverage multiple models
* to fulfill a user generation request (e.g., use speculation decoding).
* For each request, we isolate its states (e.g. the generated tokens)
* on each model. This is to say, we use RequestModelState to store
* the state of a user request on a single model (rather than all models).
*/
class RequestModelStateNode : public Object {
public:
/*! \brief The request that this state corresponds to. */
Request request;
/*!
* \brief The internal request id of this state.
* It is the **physical index** of the request in the running request queue.
* If the request is on hold (not in the running queue), the request id
* should be -1.
*/
int64_t internal_id = -1;
/*! \brief The corresponding model id of this state. */
int model_id = -1;
/*!
* \brief The committed generated token ids and related probability info.
* A token is "committed" means it will no longer be updated (or changed).
*/
std::vector<SampleResult> committed_tokens;
/*! \brief The list of input data yet for the model to prefill. */
Array<Data> inputs;
/*! \brief The list of prefilled input data, used to notify prefix cache. */
std::vector<Data> prefilled_inputs;
/*! \brief The number of tokens already cached in prefix cache. */
int64_t cached_committed_tokens = 0;
/*! \brief The number of tokens that is already prefilled from the inputs. */
int64_t num_prefilled_tokens = 0;
/*! \brief The number of tokens that need to be processed in the next decoding. */
int num_tokens_for_next_decode = 0;
/*! \brief Whether retokenization is needed in the next decoding. When the jump-forward decoding
* is enabled, retokenization is needed after every jump-forward and decoding action. */
bool require_retokenization_in_next_decode = false;
// NOTE: The following fields are reserved for future speculative inference
// settings, and are produced by the speculative small models.
/*!
* \brief The draft generated token ids and related probability info,
* which are usually generated by "small" speculative models.
* These tokens will be fed to a "large" model to determine the final
* result of speculation.
*/
std::vector<SampleResult> draft_output_tokens;
/*! \brief The storage slots for the associated states of draft tokens. */
std::vector<int> draft_token_slots;
/*! \brief The parent indices of the draft tokens. */
std::vector<int64_t> draft_token_parent_idx;
/*! \brief The first child indices of the draft tokens. */
std::vector<int64_t> draft_token_first_child_idx;
/*! \brief The appeared committed and draft tokens and their occurrence times. */
std::unordered_map<int32_t, int32_t> appeared_token_ids;
/*!
* \brief The current state of the generated token matching the grammar. Used in grammar-guided
* generation, otherwise it's std::nullopt.
*/
std::optional<xgrammar::GrammarMatcher> grammar_matcher;
/*! \brief Return the total length of the input data. */
int GetInputLength() const;
/*!
* \brief Return whether the next token bitmask is required, i.e. the grammar-guided generation is
* enabled.
*/
bool RequireNextTokenBitmask();
/*!
* \brief Find the next token bitmask and store it in the given DLTensor.
* \param bitmask The DLTensor to store the next token bitmask. The bitmask should be a tensor
* with dtype uint32_t and shape (ceildiv(vocab_size, 32),).
*/
void GetNextTokenBitmask(DLTensor* bitmask);
/*! \brief Commit a new token into committed_tokens. Does not effect the kv cache. Update
* appeared_token_ids and the grammar state. */
void CommitToken(SampleResult sampled_token);
/*! \brief Roll back the last tokens back from committed_tokens. Does not effect the kv cache.
* Also roll back appeared_token_ids and the grammar state. */
void RollbackTokens(int count);
/*! \brief Add a draft token into draft_output_tokens. Update appeared_token_ids. */
void AddDraftToken(SampleResult sampled_token, int draft_token_slot, int64_t parent_idx);
/*! \brief Remove all draft tokens from draft_output_tokens. Update appeared_token_ids. */
void RemoveAllDraftTokens(std::vector<int>* removed_draft_token_slots = nullptr);
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<RequestModelStateNode>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.RequestModelState", RequestModelStateNode, Object);
};
class RequestModelState : public ObjectRef {
public:
explicit RequestModelState(Request request, int model_id, int64_t internal_id, Array<Data> inputs,
const std::optional<xgrammar::CompiledGrammar>& compiled_grammar);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(RequestModelState, ObjectRef, RequestModelStateNode);
};
struct DeltaRequestReturn {
std::vector<int64_t> delta_token_ids;
std::vector<String> delta_logprob_json_strs;
Optional<String> finish_reason;
/*! \brief The extra string to prepend the delta output. The delta output should be
* extra_prefix_string + detokenize(delta_token_ids). */
String extra_prefix_string = "";
};
/****************** Request States ******************/
/*!
* \brief For each request, we maintain its "request state" in the
* engine. Generally, the state of a request contains the information
* of the request's generation at the current moment, including
* the generated token ids, the grammar handler, etc.
*
* When a request has multiple parallel generations (e.g., the field
* `n` of its generation config is more than 1), each generation will
* have different states all the time.
*
* Therefore, to better support parallel generations, we denote the
* state of a single generation as a "RequestStateEntry" instance,
* and denote the state of a request's all generations using a vector,
* named as a "RequestState" instance.
*
* A request's all state entries are organized as a tree structure
* when there are parallel generations.
* - the request input has the root status entry,
* - each parallel generation is a child of the root.
* This tree structure may be further extended to more complicated
* cases in the future. As of now, for the case of `n > 1`, there
* will be (n + 1) entries in total. In a "RequestState", the root
* entry always has index 0. And we guarantee that the entry order
* from the vector begin to the end is always a topological order
* of the tree.
*/
/*! \brief Request state status. */
enum class RequestStateStatus : int {
kPending = 0,
kAlive = 1,
kFinished = 2,
};
/*! \brief The data structures for each request used in the action post-process. */
struct RequestActionPostProcWorkspace {
std::vector<RequestStreamOutput> stream_outputs;
RequestStreamOutput GetStreamOutput();
};
// forward declare request state node.
class RequestStateNode;
/*!
* \brief A request's state entry. It contains the state of a single
* generation of a request, or the state of a prompt prefix of a request.
*/
class RequestStateEntryNode : public Object {
public:
/*! \brief The status of the request state entry. */
RequestStateStatus status;
/*! \brief The request that this state corresponds to. */
Request request;
/*!
* \brief The idx of the parent request state entry of this state.
* Being -1 means the state has no parent and is the foremost
* "prefix" entry or the only entry.
*/
int parent_idx = -1;
/*! \brief The children indices of the request state entry. */
std::vector<int> child_indices;
/*!
* \brief The state with regard to each model.
* \sa RequestModelState
*/
Array<RequestModelState> mstates;
/*! \brief The random number generator of this request state entry. */
RandomGenerator rng;
/*! \brief The stop string handler of this request state entry. */
StopStrHandler stop_str_handler;
/*!
* \brief The start position of the committed tokens in the
* next request stream callback invocation.
*/
int next_callback_token_pos;
/*! \brief The extra string to prepend the output. */
std::string extra_prefix_string;
std::vector<int32_t> token_ids_for_prefix_cache_update;
/*!
* \brief Back reference to the request state.
* Use ObjectRef to avoid circulate reference.
*/
RequestStateNode* rstate = nullptr;
/*!
* \brief Get the delta token ids and the logprob JSON strings for this request to return since
* the last time calling into this function, and return the finish reason if the request
* generation has finished.
* \note This function follows the destination passing style, which means it writes the
* output into the "idx"-th slot in "delta_stream_output".
* We adopt the destination passing style to reduce the CPU data structure allocation and
* construction overhead.
* \param tokenizer The tokenizer for logprob process.
* \param max_single_sequence_length The maximum allowed single sequence length.
* \param delta_stream_output The delta token ids to return, the logprob JSON strings
* of each delta token id, and the optional finish reason.
* \param idx The index denoting which slot to write results in "delta_request_return".
*/
void GetDeltaRequestReturn(const Tokenizer& tokenizer, int64_t max_single_sequence_length,
RequestStreamOutput* delta_stream_output, int idx);
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<RequestStateEntryNode>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("mlc.serve.RequestStateEntry", RequestStateEntryNode, Object);
};
class RequestStateEntry : public ObjectRef {
public:
explicit RequestStateEntry(Request request, int num_models, int64_t internal_id, int rng_seed,
const std::vector<std::string>& token_table,
const std::optional<xgrammar::CompiledGrammar>& compiled_grammar,
int parent_idx = -1);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(RequestStateEntry, ObjectRef, RequestStateEntryNode);
};
/*! \brief A request's state, which groups all the request state entries. */
class RequestStateNode : public Object {
public:
/*! \brief the request state entries */
std::vector<RequestStateEntry> entries;
/*! \brief tracks the request metrics. */
RequestMetrics metrics;
/*!
* \brief The post-process data structures.
* We make it a state to avoid repetitive memory allocation/free in the action post process.
*/
RequestActionPostProcWorkspace postproc_states;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<RequestStateNode>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("mlc.serve.RequestState", RequestStateNode, Object);
};
class RequestState : public ObjectRef {
public:
/*!
* \brief Request state constructor. We take the number of response (namely "n" in OpenAI
* API) to pre-allocate all the data structure, in order to reduce the CPU data structure
* allocation overhead when updating the request state.
*/
explicit RequestState(std::vector<RequestStateEntry> entries, int num_response,
std::chrono::high_resolution_clock::time_point add_time_point);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(RequestState, ObjectRef, RequestStateNode);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_REQUEST_STATE_H_
+588
View File
@@ -0,0 +1,588 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/sampler/cpu_sampler.cc
* \brief The implementation for CPU sampler functions.
*/
#include <tvm/ffi/function.h>
#include <tvm/runtime/tensor.h>
#include <algorithm>
#include <cmath>
#include "../../support/random.h"
#include "../../support/threading_backend.h"
#include "sampler.h"
namespace mlc {
namespace llm {
namespace serve {
TVM_FFI_STATIC_INIT_BLOCK() { SamplerObj::RegisterReflection(); }
/*!
* \brief Sample a value from the input probability distribution with top-p.
* The input is a batch of distributions, and we use `unit_offset` to specify
* which distribution to sample from.
* \param prob The input batch of probability distributions.
* \param unit_offset The offset specifying which distribution to output
* \param input_prob_offset The offset specifying which distribution to sample from.
* \param top_p The top-p value of sampling.
* \param uniform_sample The random number in [0, 1] for sampling.
* \return The sampled value and probability.
* \note This function is an enhancement of SampleTopPFromProb in TVM Unity.
* We will upstream the enhancement after it gets stable.
*/
TokenProbPair SampleTopPFromProb(Tensor prob, int unit_offset, int input_prob_offset, double top_p,
double uniform_sample) {
// prob: (*, v)
// The prob array may have arbitrary ndim and shape.
// The last dimension corresponds to the prob distribution size.
// We use the `unit_offset` parameter to determine which slice
// of the prob array we sample from.
TVM_FFI_ICHECK(prob.IsContiguous());
TVM_FFI_ICHECK(prob.DataType() == (DLDataType{kDLFloat, 32, 1}));
TVM_FFI_ICHECK_EQ(prob->device.device_type, DLDeviceType::kDLCPU);
int64_t ndata = prob->shape[prob->ndim - 1];
const float* __restrict p_prob =
static_cast<float*>(__builtin_assume_aligned(prob->data, 4)) + (input_prob_offset * ndata);
constexpr double one = 1.0f - 1e-5f;
if (top_p == 0) {
// Specially handle case where top_p == 0.
// This case is equivalent to doing argmax.
int argmax_pos = -1;
float max_prob = 0.0;
float sum_prob = 0.0;
for (int i = 0; i < ndata; ++i) {
if (p_prob[i] > max_prob) {
max_prob = p_prob[i];
argmax_pos = i;
}
// Early exit.
sum_prob += p_prob[i];
if (1 - sum_prob <= max_prob) {
break;
}
}
return {argmax_pos, 1.0};
}
if (top_p >= one) {
// Specially handle case where top_p == 1.
double prob_sum = 0.0f;
for (int64_t i = 0; i < ndata; ++i) {
prob_sum += p_prob[i];
if (prob_sum >= uniform_sample) {
return {i, p_prob[i]};
}
}
TVM_FFI_ICHECK(false) << "Possibly prob distribution contains NAN.";
}
// Key observation: when we are doing top_p sampling
// usually we only need to preserve some of the elements with
// high probabilities before we do sort
thread_local std::vector<std::pair<float, int>> data;
auto sample_top_p_with_filter = [&](float cuttoff) -> std::pair<float, int64_t> {
data.clear();
// filter the data with cuttoff
float cutoff_sum = 0.0f;
for (int64_t i = 0; i < ndata; ++i) {
if (p_prob[i] >= cuttoff) {
cutoff_sum += p_prob[i];
data.emplace_back(std::make_pair(p_prob[i], static_cast<int>(i)));
if (cutoff_sum > 1 - cuttoff) {
// Short cut. When the remaining parts cannot have total
// probability larger than cutoff, we can quit.
break;
}
}
}
if (data.size() == 0) return std::make_pair(-1, -1);
auto fcmp = [](const std::pair<float, int>& lhs, const std::pair<float, int>& rhs) {
return lhs.first > rhs.first;
};
std::sort(data.begin(), data.end(), fcmp);
// short cut, if we know that
// uniform sample < p[0] / top_p
// we know that unform_sample < p[0] / top_p_sum
// because top_p_sum guarantees to be smaller than top_p
// so we can simply return the argmax sample
// without computing anything
if (uniform_sample < data[0].first / top_p) {
return std::make_pair(data[0].first, data[0].second);
}
// compute top_p_sum
float cum_sum_prob = 0.0f;
float top_p_sum = 0.0f;
for (auto it = data.begin(); it != data.end(); ++it) {
float prob = it->first;
if (cum_sum_prob < top_p) {
top_p_sum += prob;
} else {
// we get to the right cutoff pt
break;
}
cum_sum_prob += prob;
it->first = cum_sum_prob;
}
// we find that the current total sum by the given cutoff
// is not sufficient to cover everything
// this means we might need to retry a smaller cutoff pt.
if (cum_sum_prob < top_p && cuttoff != 0.0f) return std::make_pair(-1, -1);
float last_cum_sum_prob = 0.0;
for (auto it = data.begin(); it != data.end(); ++it) {
if (uniform_sample < it->first / top_p_sum) {
return std::make_pair(it->first - last_cum_sum_prob, it->second);
}
last_cum_sum_prob = it->first;
}
return std::make_pair(data[static_cast<int64_t>(data.size()) - 1].first - last_cum_sum_prob,
data[static_cast<int64_t>(data.size()) - 1].second);
};
if (top_p < 1) {
// sample through cutoff by a number
// by pigeonhole principle we will get at most 1024 elements
// usually it is much less by applying this filtering(order of 10 - 20)
data.reserve(256);
std::pair<float, int64_t> sampled_index = sample_top_p_with_filter(top_p / 1024);
if (sampled_index.second >= 0) return {sampled_index.second, sampled_index.first};
}
// fallback via full prob, rare case
data.reserve(ndata);
std::pair<float, int64_t> sampled_index = sample_top_p_with_filter(0.0f);
TVM_FFI_ICHECK_GE(sampled_index.second, 0);
return {sampled_index.second, sampled_index.first};
}
/*!
* \brief Renormalize the probability distribution by the top p value.
* \param prob The input batch of probability distributions.
* \param unit_offset The offset specifying which distribution to output
* \param top_p The top p value for renormalization.
* \param eps A small epsilon value for comparison stability.
*/
void RenormalizeProbByTopP(Tensor prob, int unit_offset, double top_p, double eps) {
// prob: (*, v)
// The prob array may have arbitrary ndim and shape.
// The last dimension corresponds to the prob distribution size.
// We use the `unit_offset` parameter to determine which slice
// of the prob array we will renormalize.
TVM_FFI_ICHECK(prob.IsContiguous());
TVM_FFI_ICHECK(prob.DataType() == (DLDataType{kDLFloat, 32, 1}));
TVM_FFI_ICHECK_EQ(prob->device.device_type, DLDeviceType::kDLCPU);
if (top_p == 1.0) {
// No renormalization is needed if top_p is 1.
return;
}
int vocab_size = prob->shape[prob->ndim - 1];
float* __restrict p_prob =
static_cast<float*>(__builtin_assume_aligned(prob->data, 4)) + (unit_offset * vocab_size);
// We manually choice the cutoff values of "top_p / 256" and "top_p / 8192".
// In most of the cases, only one round is needed.
std::vector<double> cutoff_values{top_p / 256, top_p / 8192, 0.0f};
// Create the upper partition vector and the lower partition rolling vectors.
std::vector<float> upper_partition;
std::vector<float> lower_partitions[2];
upper_partition.reserve(vocab_size);
lower_partitions[0].reserve(vocab_size);
lower_partitions[1].reserve(vocab_size);
float upper_partition_sum = 0.0;
for (int round = 0; round < static_cast<int>(cutoff_values.size()); ++round) {
const float* lower_partition_begin;
const float* lower_partition_end;
if (round == 0) {
lower_partition_begin = p_prob;
lower_partition_end = p_prob + vocab_size;
} else {
int idx = (round - 1) & 1;
lower_partition_begin = lower_partitions[idx].data();
lower_partition_end = lower_partitions[idx].data() + lower_partitions[idx].size();
}
// - Partition the last round lower partition into upper and lower
// based on the new cutoff value.
std::vector<float>& lower_partition = lower_partitions[round & 1];
lower_partition.clear();
for (const float* ptr = lower_partition_begin; ptr != lower_partition_end; ++ptr) {
if (*ptr >= cutoff_values[round]) {
upper_partition.push_back(*ptr);
upper_partition_sum += *ptr;
} else {
lower_partition.push_back(*ptr);
}
}
// - If the upper partition sum is at least top p, exit the loop.
if (upper_partition_sum >= top_p - eps) {
break;
}
}
// - Sort the upper partition in descending order.
std::sort(upper_partition.begin(), upper_partition.end(), std::greater<>());
// - Find the top p boundary prob value.
float boundary_value = -1.0;
upper_partition_sum = 0.0;
for (float upper_value : upper_partition) {
upper_partition_sum += upper_value;
if (upper_partition_sum >= top_p - eps) {
boundary_value = upper_value;
break;
}
}
// - Mask all values smaller than the boundary to 0.
float renormalize_sum = 0.0;
std::vector<int> upper_partition_indices;
upper_partition_indices.reserve(vocab_size);
for (int i = 0; i < vocab_size; ++i) {
if (p_prob[i] >= boundary_value) {
upper_partition_indices.push_back(i);
renormalize_sum += p_prob[i];
} else {
p_prob[i] = 0.0;
}
}
// - Renormalize.
for (int idx : upper_partition_indices) {
p_prob[idx] /= renormalize_sum;
}
}
namespace detail {
/*! \brief Implementation of getting top probs on CPU. */
template <int num_top_probs>
std::vector<TokenProbPair> ComputeTopProbsImpl(const float* p_prob, int ndata) {
std::vector<TokenProbPair> top_probs;
top_probs.reserve(num_top_probs);
for (int i = 0; i < num_top_probs; ++i) {
top_probs.emplace_back(-1, -1.0f);
}
float sum_prob = 0.0;
// Selection argsort.
for (int p = 0; p < ndata; ++p) {
int i = num_top_probs - 1;
for (; i >= 0; --i) {
if (p_prob[p] > top_probs[i].second) {
if (i != num_top_probs - 1) {
top_probs[i + 1] = top_probs[i];
}
} else {
break;
}
}
if (i != num_top_probs - 1) {
top_probs[i + 1] = {p, p_prob[p]};
}
// Early exit.
sum_prob += p_prob[p];
if (1 - sum_prob <= top_probs[num_top_probs - 1].second) {
break;
}
}
return top_probs;
}
} // namespace detail
/*! \brief Get the probs of a few number of tokens with top probabilities. */
inline std::vector<TokenProbPair> ComputeTopProbs(Tensor prob, int unit_offset, int num_top_probs) {
TVM_FFI_ICHECK_LE(num_top_probs, 5);
TVM_FFI_ICHECK_EQ(prob->ndim, 2);
int ndata = prob->shape[1];
const float* __restrict p_prob =
static_cast<float*>(__builtin_assume_aligned(prob->data, 4)) + (unit_offset * ndata);
switch (num_top_probs) {
case 0:
return {};
case 1:
return detail::ComputeTopProbsImpl<1>(p_prob, ndata);
case 2:
return detail::ComputeTopProbsImpl<2>(p_prob, ndata);
case 3:
return detail::ComputeTopProbsImpl<3>(p_prob, ndata);
case 4:
return detail::ComputeTopProbsImpl<4>(p_prob, ndata);
case 5:
return detail::ComputeTopProbsImpl<5>(p_prob, ndata);
}
throw;
}
/********************* CPU Sampler *********************/
class CPUSampler : public SamplerObj {
public:
explicit CPUSampler(Optional<EventTraceRecorder> trace_recorder)
: trace_recorder_(std::move(trace_recorder)) {}
Tensor BatchRenormalizeProbsByTopP(Tensor probs_on_device, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg) final {
// probs_on_device: (n, v)
TVM_FFI_ICHECK_EQ(probs_on_device->ndim, 2);
// - Copy probs to CPU
RECORD_EVENT(trace_recorder_, request_ids, "start copy probs to CPU");
Tensor probs_on_host = CopyProbsToCPU(probs_on_device);
RECORD_EVENT(trace_recorder_, request_ids, "finish copy probs to CPU");
int num_samples = sample_indices.size();
int num_probs = probs_on_device->shape[0];
int vocab_size = probs_on_device->shape[1];
TVM_FFI_ICHECK_EQ(request_ids.size(), num_samples);
TVM_FFI_ICHECK_EQ(generation_cfg.size(), num_samples);
std::vector<int> top_p_indices;
std::vector<double> top_p_values;
for (int i = 0; i < num_samples; ++i) {
if (top_p_indices.empty() || top_p_indices.back() != sample_indices[i]) {
top_p_indices.push_back(sample_indices[i]);
top_p_values.push_back(generation_cfg[i]->top_p);
} else {
TVM_FFI_ICHECK(fabs(top_p_values.back() - generation_cfg[i]->top_p) < eps_)
<< "Sampler requires the top_p values for each prob distribution are the same.";
}
}
if (top_p_indices.empty()) {
// Return if no top p needs to apply.
return probs_on_host;
}
tvm::runtime::parallel_for_with_threading_backend(
[this, &probs_on_host, &request_ids, &top_p_indices, &top_p_values](int i) {
RECORD_EVENT(this->trace_recorder_, request_ids[i], "start renormalize by top p");
RenormalizeProbByTopP(probs_on_host, top_p_indices[i], top_p_values[i], eps_);
RECORD_EVENT(this->trace_recorder_, request_ids[i], "finish renormalize by top p");
},
0, static_cast<int64_t>(top_p_indices.size()));
return probs_on_host;
}
std::vector<SampleResult> BatchSampleTokensWithProbBeforeTopP(
Tensor probs_on_device, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg, //
const std::vector<RandomGenerator*>& rngs) final {
// probs_on_device: (n, v)
TVM_FFI_ICHECK_EQ(probs_on_device->ndim, 2);
// - Copy probs to CPU
RECORD_EVENT(trace_recorder_, request_ids, "start copy probs to CPU");
Tensor probs_on_host = CopyProbsToCPU(probs_on_device);
RECORD_EVENT(trace_recorder_, request_ids, "finish copy probs to CPU");
return BatchSampleTokensImpl(probs_on_host, sample_indices, request_ids, generation_cfg, rngs,
/*top_p_applied=*/false);
}
std::vector<SampleResult> BatchSampleTokensWithProbAfterTopP(
Tensor probs_on_host, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg, //
const std::vector<RandomGenerator*>& rngs) final {
return BatchSampleTokensImpl(probs_on_host, sample_indices, request_ids, generation_cfg, rngs,
/*top_p_applied=*/true);
}
std::pair<std::vector<std::vector<SampleResult>>, std::vector<int>>
BatchVerifyDraftTokensWithProbAfterTopP(
Tensor probs_on_host, const Array<String>& request_ids,
const std::vector<int>& cum_verify_lengths, const Array<GenerationConfig>& generation_cfg,
const std::vector<RandomGenerator*>& rngs,
const std::vector<std::vector<SampleResult>>& draft_output_tokens,
const std::vector<int64_t>& token_tree_parent_ptr, Tensor draft_probs_on_device) final {
// probs_on_host: (n, v)
RECORD_EVENT(trace_recorder_, request_ids, "start draft verification");
TVM_FFI_ICHECK_EQ(probs_on_host->ndim, 2);
int num_sequence = static_cast<int>(cum_verify_lengths.size()) - 1;
TVM_FFI_ICHECK_EQ(rngs.size(), num_sequence);
TVM_FFI_ICHECK_EQ(draft_output_tokens.size(), num_sequence);
Tensor draft_probs_on_host = draft_probs_on_device.CopyTo(DLDevice{kDLCPU, 0});
std::vector<std::vector<SampleResult>> sample_results;
sample_results.resize(num_sequence);
float* __restrict global_p_probs =
static_cast<float*>(__builtin_assume_aligned(probs_on_host->data, 4));
int vocab_size = probs_on_host->shape[1];
std::vector<int> last_accepted_tree_node(num_sequence, 0);
tvm::runtime::parallel_for_with_threading_backend(
[&](int i) {
int verify_start = cum_verify_lengths[i];
int verify_end = cum_verify_lengths[i + 1];
TVM_FFI_ICHECK_EQ(token_tree_parent_ptr[verify_start], -1);
for (int j = verify_start + 1; j < verify_end; ++j) {
TVM_FFI_ICHECK_EQ(token_tree_parent_ptr[j], j - verify_start - 1)
<< "CPU sampler only supports chain-style draft tokens.";
}
int cur_token_idx = 0;
// Sub 1 to ignore the last prediction.
for (; cur_token_idx < verify_end - verify_start - 1; ++cur_token_idx) {
float* p_probs = global_p_probs + (verify_start + cur_token_idx) * vocab_size;
int cur_token = draft_output_tokens[i][cur_token_idx].GetTokenId();
float q_value = draft_output_tokens[i][cur_token_idx].sampled_token_id.second;
float p_value = p_probs[cur_token];
if (p_value >= q_value) {
sample_results[i].push_back(
SampleResult{{cur_token, p_value},
ComputeTopProbs(probs_on_host, verify_start + cur_token_idx,
generation_cfg[i]->top_logprobs)});
continue;
}
float r = rngs[i]->GetRandomNumber();
if (r < p_value / (q_value + eps_)) {
sample_results[i].push_back(
SampleResult{{cur_token, p_value},
ComputeTopProbs(probs_on_host, verify_start + cur_token_idx,
generation_cfg[i]->top_logprobs)});
continue;
}
// normalize a new probability distribution
double sum_v = 0.0;
const float* __restrict p_qdist =
static_cast<float*>(__builtin_assume_aligned(draft_probs_on_host->data, 4)) +
(verify_start + cur_token_idx + 1) * vocab_size;
for (int j = 0; j < vocab_size; ++j) {
p_probs[j] = std::max(p_probs[j] - p_qdist[j], 0.0f);
sum_v += p_probs[j];
}
for (int j = 0; j < vocab_size; ++j) {
p_probs[j] /= sum_v;
}
// sample a new token from the new distribution
SampleResult sample_result;
sample_result.sampled_token_id = SampleTopPFromProb(
probs_on_host, verify_start + cur_token_idx, verify_start + cur_token_idx,
/*top_p=*/1.0f, rngs[i]->GetRandomNumber());
sample_result.top_prob_tokens = ComputeTopProbs(
probs_on_host, verify_start + cur_token_idx, generation_cfg[i]->top_logprobs);
sample_results[i].push_back(sample_result);
break;
}
last_accepted_tree_node[i] = cur_token_idx;
// if cur_token_idx == verify_end - verify_start - 1
// all draft tokens are accepted
// we sample a new token
if (cur_token_idx == verify_end - verify_start - 1) {
SampleResult sample_result;
// sample a new token from the original distribution
sample_result.sampled_token_id = SampleTopPFromProb(
probs_on_host, verify_start + cur_token_idx, verify_start + cur_token_idx,
/*top_p=*/1.0f, rngs[i]->GetRandomNumber());
sample_result.top_prob_tokens = ComputeTopProbs(
probs_on_host, verify_start + cur_token_idx, generation_cfg[i]->top_logprobs);
sample_results[i].push_back(sample_result);
}
},
0, num_sequence);
RECORD_EVENT(trace_recorder_, request_ids, "finish draft verification");
return {sample_results, last_accepted_tree_node};
}
private:
std::vector<SampleResult> BatchSampleTokensImpl(Tensor probs_on_host, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg, //
const std::vector<RandomGenerator*>& rngs, //
bool top_p_applied) {
// probs_on_host: (n, v)
RECORD_EVENT(trace_recorder_, request_ids, "start sampling");
TVM_FFI_ICHECK_EQ(probs_on_host->ndim, 2);
TVM_FFI_ICHECK_EQ(probs_on_host->device.device_type, DLDeviceType::kDLCPU);
// - Sample tokens from probabilities.
int n = request_ids.size();
TVM_FFI_ICHECK_EQ(generation_cfg.size(), n);
TVM_FFI_ICHECK_EQ(rngs.size(), n);
std::vector<SampleResult> sample_results;
sample_results.resize(n);
tvm::runtime::parallel_for_with_threading_backend(
[this, &sample_results, &probs_on_host, &generation_cfg, &rngs, &request_ids, top_p_applied,
sample_indices](int i) {
RECORD_EVENT(this->trace_recorder_, request_ids[i], "start sample token");
// Sample top p from probability.
double top_p =
top_p_applied
? 1.0f
: (generation_cfg[i]->temperature < eps_ ? 0.0 : generation_cfg[i]->top_p);
sample_results[i].sampled_token_id = SampleTopPFromProb(
probs_on_host, i, sample_indices[i], top_p, rngs[i]->GetRandomNumber());
sample_results[i].top_prob_tokens =
ComputeTopProbs(probs_on_host, i, generation_cfg[i]->top_logprobs);
RECORD_EVENT(this->trace_recorder_, request_ids[i], "finish sample token");
},
0, n);
RECORD_EVENT(trace_recorder_, request_ids, "finish sampling");
return sample_results;
}
/*! \brief Copy prob distributions from device to CPU. */
Tensor CopyProbsToCPU(Tensor probs_on_device) {
// probs_on_device: (n, v)
if (probs_on_device->device.device_type == kDLCPU) {
return probs_on_device;
}
TVM_FFI_ICHECK(probs_on_device->device.device_type != kDLCPU);
if (probs_host_.defined()) {
TVM_FFI_ICHECK_EQ(probs_host_->shape[1], probs_on_device->shape[1]);
}
int64_t init_size = probs_host_.defined() ? probs_host_->shape[0] : 32;
int64_t num_tokens = probs_on_device->shape[0];
int64_t vocab_size = probs_on_device->shape[1];
while (init_size < num_tokens) {
init_size *= 2;
}
if (!probs_host_.defined() || init_size != probs_host_->shape[0]) {
probs_host_ =
Tensor::Empty({init_size, vocab_size}, probs_on_device->dtype, DLDevice{kDLCPU, 0});
}
TVM_FFI_ICHECK_LE(num_tokens, probs_host_->shape[0]);
Tensor view = probs_host_.CreateView({num_tokens, vocab_size}, probs_on_device->dtype);
view.CopyFrom(probs_on_device);
return view;
}
/*! \brief The event trace recorder for requests. */
Optional<EventTraceRecorder> trace_recorder_;
/*! \brief Customized function which computes prob distribution from logits */
Function flogits_to_probs_inplace_;
/*! \brief Probability distribution array on CPU. */
Tensor probs_host_{nullptr};
const float eps_ = 1e-5;
};
Sampler Sampler::CreateCPUSampler(Optional<EventTraceRecorder> trace_recorder) {
return Sampler(tvm::ffi::make_object<CPUSampler>(std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
+756
View File
@@ -0,0 +1,756 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/sampler/gpu_sampler.cc
* \brief The implementation for GPU sampler functions.
*/
#include <tvm/ffi/function.h>
#include <tvm/runtime/device_api.h>
#include <tvm/runtime/tensor.h>
#include <tvm/support/cuda/nvtx.h>
#include "../../support/random.h"
#include "sampler.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::support::NVTXScopedRange;
inline bool FlashInferSamplingAvailable(Device device) {
// Device must be CUDA, and FlashInfer must be enabled.
if (device.device_type != DLDeviceType::kDLCUDA ||
!Function::GetGlobal("flashinfer.sampling.parallel_sampling_from_prob").has_value()) {
return false;
}
// Compute version must be at least 8.0
Any rv;
DeviceAPI::Get(device)->GetAttr(device, kComputeVersion, &rv);
std::string compute_version = rv.cast<std::string>();
std::string major_version = compute_version.substr(0, compute_version.find('.'));
return std::stoi(major_version) >= 8;
}
inline void CopyArray(Tensor src, Tensor dst, TVMStreamHandle copy_stream) {
DLTensor dl_dst = *(dst.operator->());
Tensor::CopyFromTo(src.operator->(), &dl_dst, copy_stream);
}
inline void SyncCopyStream(Device device, TVMStreamHandle compute_stream,
TVMStreamHandle copy_stream) {
// - If there is no particular copy stream, no action is needed.
if (copy_stream == nullptr) {
return;
}
// - Sync two streams.
DeviceAPI::Get(device)->SyncStreamFromTo(device, copy_stream, compute_stream);
}
/*********************** GPU Sampler ***********************/
class GPUSampler : public SamplerObj {
public:
explicit GPUSampler(int max_num_sample, int vocab_size, FunctionTable* ft, DLDevice device,
Optional<EventTraceRecorder> trace_recorder)
: max_num_sample_(max_num_sample),
vocab_size_(vocab_size),
flashinfer_sampling_available_(FlashInferSamplingAvailable(device)),
device_(device),
gpu_multinomial_from_uniform_func_(ft->gpu_multinomial_from_uniform_func_),
gpu_argsort_probs_func_(ft->gpu_argsort_probs_func_),
gpu_sample_with_top_p_func_(ft->gpu_sample_with_top_p_func_),
gpu_sampler_take_probs_func_(ft->gpu_sampler_take_probs_func_),
gpu_verify_draft_tokens_func_(ft->gpu_verify_draft_tokens_func_),
gpu_renormalize_by_top_p_func_(ft->gpu_renormalize_by_top_p_func_),
trace_recorder_(std::move(trace_recorder)) {
TVM_FFI_ICHECK(gpu_multinomial_from_uniform_func_.defined());
TVM_FFI_ICHECK(gpu_argsort_probs_func_.defined());
TVM_FFI_ICHECK(gpu_sample_with_top_p_func_.defined());
TVM_FFI_ICHECK(gpu_sampler_take_probs_func_.defined());
flashinfer_multinomial_sample_func_ =
Function::GetGlobal("flashinfer.sampling.parallel_sampling_from_prob");
Device preferred_host_device = GetPreferredHostDevice(device);
// We support at most 5 top prob results for each sequence.
// Initialize auxiliary arrays on CPU.
uniform_samples_host_ = Tensor::Empty({max_num_sample}, dtype_f32_, preferred_host_device);
sample_indices_host_ = Tensor::Empty({max_num_sample}, dtype_i32_, preferred_host_device);
top_p_host_ = Tensor::Empty({max_num_sample}, dtype_f32_, preferred_host_device);
top_p_init_pivots_host_ = Tensor::Empty({max_num_sample, num_top_p_cutoff_pivots_}, dtype_f32_,
preferred_host_device);
top_prob_offsets_host_ = Tensor::Empty({max_num_sample * 5}, dtype_i32_, preferred_host_device);
draft_tokens_host_ = Tensor::Empty({max_num_sample}, dtype_i32_, preferred_host_device);
token_tree_first_child_host_ =
Tensor::Empty({max_num_sample}, dtype_i32_, preferred_host_device);
token_tree_next_sibling_host_ =
Tensor::Empty({max_num_sample}, dtype_i32_, preferred_host_device);
token_tree_parent_ptr_host_ =
Tensor::Empty({max_num_sample}, dtype_i32_, preferred_host_device);
sampled_token_ids_host_ = Tensor::Empty({max_num_sample}, dtype_i32_, preferred_host_device);
sampled_probs_host_ = Tensor::Empty({max_num_sample}, dtype_f32_, preferred_host_device);
top_prob_probs_host_ = Tensor::Empty({max_num_sample * 5}, dtype_f32_, preferred_host_device);
top_prob_indices_host_ = Tensor::Empty({max_num_sample * 5}, dtype_i32_, preferred_host_device);
// Initialize auxiliary arrays on GPU.
uniform_samples_device_ = Tensor::Empty({max_num_sample}, dtype_f32_, device);
sample_indices_device_ = Tensor::Empty({max_num_sample}, dtype_i32_, device);
top_p_device_ = Tensor::Empty({max_num_sample}, dtype_f32_, device);
top_p_init_pivots_device_ =
Tensor::Empty({max_num_sample, num_top_p_cutoff_pivots_}, dtype_f32_, device);
top_prob_offsets_device_ = Tensor::Empty({max_num_sample * 5}, dtype_i32_, device);
draft_tokens_device_ = Tensor::Empty({max_num_sample}, dtype_i32_, device);
token_tree_first_child_device_ = Tensor::Empty({max_num_sample}, dtype_i32_, device);
token_tree_next_sibling_device_ = Tensor::Empty({max_num_sample}, dtype_i32_, device);
token_tree_parent_ptr_device_ = Tensor::Empty({max_num_sample}, dtype_i32_, device);
sampled_token_ids_device_ = Tensor::Empty({max_num_sample}, dtype_i32_, device);
// If the device is CUDA/ROCm, we create a standalone copy stream, in
// purpose to hide the latency of auxiliary stream copy.
if (device.device_type == DLDeviceType::kDLCUDA ||
device.device_type == DLDeviceType::kDLROCM) {
// The compute stream is the default stream.
compute_stream_ = DeviceAPI::Get(device)->GetCurrentStream(device);
copy_stream_ = DeviceAPI::Get(device)->CreateStream(device);
}
}
~GPUSampler() {
// Free the copy stream if defined.
if (copy_stream_ != nullptr) {
DeviceAPI::Get(device_)->FreeStream(device_, copy_stream_);
}
}
Tensor BatchRenormalizeProbsByTopP(Tensor probs_on_device, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg) final {
NVTXScopedRange nvtx_scope("BatchRenormalizeProbsByTopP");
// probs_on_device: (n, v)
RECORD_EVENT(trace_recorder_, request_ids, "start renormalization by top p");
TVM_FFI_ICHECK_EQ(probs_on_device->ndim, 2);
int num_samples = sample_indices.size();
int num_probs = probs_on_device->shape[0];
int vocab_size = probs_on_device->shape[1];
TVM_FFI_ICHECK_LE(num_probs, max_num_sample_);
TVM_FFI_ICHECK_EQ(generation_cfg.size(), num_samples);
// - Check if there is need for applying top p.
bool need_top_p = CheckTopP(generation_cfg, sample_indices, num_probs, num_samples, vocab_size);
if (!need_top_p) {
return probs_on_device;
}
// - Copy auxiliary array for top-p and initial pivots.
Tensor top_p_host = top_p_host_.CreateView({num_probs}, dtype_f32_);
Tensor top_p_device = top_p_device_.CreateView({num_probs}, dtype_f32_);
CopyArray(/*src=*/top_p_host, /*dst=*/top_p_device, copy_stream_);
Tensor top_p_init_pivots_host =
top_p_init_pivots_host_.CreateView({num_probs, num_top_p_cutoff_pivots_}, dtype_f32_);
Tensor top_p_init_pivots_device =
top_p_init_pivots_device_.CreateView({num_probs, num_top_p_cutoff_pivots_}, dtype_f32_);
const float* p_top_p = static_cast<const float*>(top_p_host->data);
float* p_top_p_init_pivots = static_cast<float*>(top_p_init_pivots_host->data);
for (int i = 0; i < num_probs; ++i) {
if (1 - p_top_p[i] >= 0.02) {
p_top_p_init_pivots[i * num_top_p_cutoff_pivots_] =
std::min(1 - p_top_p[i], static_cast<float>(0.5));
p_top_p_init_pivots[i * num_top_p_cutoff_pivots_ + 1] = 0.02;
p_top_p_init_pivots[i * num_top_p_cutoff_pivots_ + 2] = 0.01;
} else {
p_top_p_init_pivots[i * num_top_p_cutoff_pivots_] = 1 - p_top_p[i];
p_top_p_init_pivots[i * num_top_p_cutoff_pivots_ + 1] = (1 - p_top_p[i]) / 2;
p_top_p_init_pivots[i * num_top_p_cutoff_pivots_ + 2] = (1 - p_top_p[i]) / 4;
}
}
CopyArray(/*src=*/top_p_init_pivots_host, /*dst=*/top_p_init_pivots_device, copy_stream_);
SyncCopyStream(device_, compute_stream_, copy_stream_);
// - Renormalize the prob with top p.
Tensor renormed_probs_on_device =
gpu_renormalize_by_top_p_func_(probs_on_device, top_p_device, top_p_init_pivots_device)
.cast<Tensor>();
RECORD_EVENT(trace_recorder_, request_ids, "finish renormalization by top p");
return renormed_probs_on_device;
}
std::vector<SampleResult> BatchSampleTokensWithProbBeforeTopP(
Tensor probs_on_device, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg, //
const std::vector<RandomGenerator*>& rngs) final {
NVTXScopedRange nvtx_scope("BatchSampleTokensWithProbBeforeTopP");
return BatchSampleTokensImpl(std::move(probs_on_device), sample_indices, request_ids,
generation_cfg, rngs, /*top_p_applied=*/false);
}
std::vector<SampleResult> BatchSampleTokensWithProbAfterTopP(
Tensor probs_on_device, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg, //
const std::vector<RandomGenerator*>& rngs) final {
NVTXScopedRange nvtx_scope("BatchSampleTokensWithProbAfterTopP");
return BatchSampleTokensImpl(std::move(probs_on_device), sample_indices, request_ids,
generation_cfg, rngs, /*top_p_applied=*/true);
}
std::pair<std::vector<std::vector<SampleResult>>, std::vector<int>>
BatchVerifyDraftTokensWithProbAfterTopP(
Tensor probs_on_device, const Array<String>& request_ids,
const std::vector<int>& cum_verify_lengths, const Array<GenerationConfig>& generation_cfg,
const std::vector<RandomGenerator*>& rngs,
const std::vector<std::vector<SampleResult>>& draft_output_tokens,
const std::vector<int64_t>& token_tree_parent_ptr, Tensor draft_probs_on_device) final {
NVTXScopedRange nvtx_scope("BatchVerifyDraftTokensWithProbAfterTopP");
std::vector<std::vector<SampleResult>> sample_results;
// probs_on_device: (n, v)
RECORD_EVENT(trace_recorder_, request_ids, "start draft verification");
TVM_FFI_ICHECK_EQ(probs_on_device->ndim, 2);
int num_sequence = static_cast<int>(cum_verify_lengths.size()) - 1;
TVM_FFI_ICHECK_EQ(rngs.size(), num_sequence);
TVM_FFI_ICHECK_EQ(draft_output_tokens.size(), num_sequence);
sample_results.resize(num_sequence);
int num_nodes = cum_verify_lengths.back();
TVM_FFI_ICHECK(num_nodes <= max_num_sample_);
TVM_FFI_ICHECK_EQ(draft_probs_on_device->shape[0], num_nodes);
Tensor uniform_samples_device = GenerateUniformSamples(rngs, cum_verify_lengths);
Tensor draft_tokens_host = draft_tokens_host_.CreateView({num_nodes}, dtype_i32_);
Tensor draft_tokens_device = draft_tokens_device_.CreateView({num_nodes}, dtype_i32_);
// Copy draft tokens to GPU
int* p_draft_tokens_host = static_cast<int*>(draft_tokens_host->data);
for (int i = 0; i < num_sequence; i++) {
const std::vector<SampleResult>& draft_output_tokens_i = draft_output_tokens[i];
int start = cum_verify_lengths[i];
int end = cum_verify_lengths[i + 1];
// start/end is the range of the sequence i in probs_on_device, which includes the prob dist
// of the draft tokens and the last committed token
TVM_FFI_ICHECK_EQ(draft_output_tokens_i.size() + 1, end - start);
for (int j = 0; j < end - start - 1; j++) {
// Copy sampled token id
p_draft_tokens_host[start + j + 1] = draft_output_tokens_i[j].GetTokenId();
}
}
CopyArray(draft_tokens_host, draft_tokens_device, copy_stream_);
Tensor token_tree_first_child_host =
token_tree_first_child_host_.CreateView({num_nodes}, dtype_i32_);
Tensor token_tree_first_child_device =
token_tree_first_child_device_.CreateView({num_nodes}, dtype_i32_);
Tensor token_tree_next_sibling_host =
token_tree_next_sibling_host_.CreateView({num_nodes}, dtype_i32_);
Tensor token_tree_next_sibling_device =
token_tree_next_sibling_device_.CreateView({num_nodes}, dtype_i32_);
Tensor token_tree_parent_ptr_host =
token_tree_parent_ptr_host_.CreateView({num_sequence}, dtype_i32_);
Tensor token_tree_parent_ptr_device =
token_tree_parent_ptr_device_.CreateView({num_sequence}, dtype_i32_);
std::vector<int> token_tree_child_to_parent(/*n=*/num_nodes);
int* token_tree_first_child_ptr_host = static_cast<int*>(token_tree_first_child_host->data);
int* token_tree_next_sibling_ptr_host = static_cast<int*>(token_tree_next_sibling_host->data);
// Build the tree structure on CPU
for (int i = 0; i < num_sequence; i++) {
// Assuming no tree structure for now
int start = cum_verify_lengths[i];
int end = cum_verify_lengths[i + 1];
TVM_FFI_ICHECK_GE(end - start, 2);
for (int j = 0; j < end - start; j++) {
int cur_node = j + start;
int parent_node =
token_tree_parent_ptr[cur_node] != -1 ? token_tree_parent_ptr[cur_node] + start : -1;
token_tree_first_child_ptr_host[cur_node] = -1;
if (parent_node != -1 && token_tree_first_child_ptr_host[parent_node] == -1) {
token_tree_first_child_ptr_host[parent_node] = cur_node;
}
token_tree_child_to_parent[cur_node] = parent_node;
if (cur_node + 1 < end && token_tree_parent_ptr[cur_node - start + 1] ==
token_tree_parent_ptr[cur_node - start]) {
token_tree_next_sibling_ptr_host[cur_node] = cur_node + 1;
} else {
token_tree_next_sibling_ptr_host[cur_node] = -1;
}
}
static_cast<int*>(token_tree_parent_ptr_host->data)[i] = start; // point to the root
}
// Copy token tree structure to GPU
CopyArray(token_tree_first_child_host, token_tree_first_child_device, copy_stream_);
CopyArray(token_tree_next_sibling_host, token_tree_next_sibling_device, copy_stream_);
CopyArray(token_tree_parent_ptr_host, token_tree_parent_ptr_device, copy_stream_);
SyncCopyStream(device_, compute_stream_, copy_stream_);
gpu_verify_draft_tokens_func_(draft_probs_on_device, draft_tokens_device, probs_on_device,
token_tree_first_child_device, token_tree_next_sibling_device,
uniform_samples_device, token_tree_parent_ptr_device);
DeviceAPI::Get(device_)->SyncStreamFromTo(device_, compute_stream_, copy_stream_);
CopyArray(token_tree_parent_ptr_device, token_tree_parent_ptr_host, copy_stream_);
std::vector<SampleResult> additional_sample_result;
{
additional_sample_result.reserve(num_sequence);
// Sample one additional token for each sequence using the probablity at the last accepted
// token.
uniform_samples_device = GenerateUniformSamples(rngs, num_sequence);
const Tensor& sample_indices_device = token_tree_parent_ptr_device;
// Check need_prob_values
bool need_prob_values = false;
for (int i = 0; i < num_sequence; i++) {
need_prob_values |= generation_cfg[i]->logprobs;
}
std::vector<int> top_prob_offset_indptr;
if (!need_prob_values) {
top_prob_offset_indptr.resize(num_sequence + 1, 0);
} else {
// Slow path: if any of the generation config requires prob values, we need to copy
// sample_indices to host to compute top_prob_offset_indptr.
DeviceAPI::Get(device_)->StreamSync(device_, copy_stream_);
std::vector<int> sample_indices;
sample_indices.reserve(num_sequence);
const int* p_token_tree_parent_ptr = static_cast<int*>(token_tree_parent_ptr_host->data);
for (int i = 0; i < num_sequence; i++) {
sample_indices.push_back(p_token_tree_parent_ptr[i]);
}
CheckProbValues(generation_cfg, sample_indices, num_nodes, num_sequence, vocab_size_,
&top_prob_offset_indptr);
}
auto device_arrays =
SampleOnGPU(probs_on_device, uniform_samples_device, sample_indices_device,
/*need_top_p=*/false, need_prob_values, num_nodes, top_prob_offset_indptr);
auto host_arrays = CopyArraysToCPU(device_arrays, num_sequence, need_prob_values,
top_prob_offset_indptr.back());
additional_sample_result =
CollectSampleResult(host_arrays, num_sequence, need_prob_values, top_prob_offset_indptr);
}
std::vector<int> last_accepted_tree_node;
last_accepted_tree_node.reserve(num_sequence);
for (int i = 0; i < num_sequence; i++) {
int start = cum_verify_lengths[i];
int end = cum_verify_lengths[i + 1];
int last_accepted = static_cast<int*>(token_tree_parent_ptr_host->data)[i];
last_accepted_tree_node.push_back(last_accepted - start);
int num_accepted = 0;
for (int cur_node = last_accepted; cur_node != start;
cur_node = token_tree_child_to_parent[cur_node]) {
sample_results[i].push_back(draft_output_tokens[i][cur_node - start - 1]);
num_accepted++;
}
std::reverse(sample_results[i].rbegin(), sample_results[i].rbegin() + num_accepted);
}
// Append the additional sample result to the sample_results
TVM_FFI_ICHECK_EQ(additional_sample_result.size(), num_sequence);
for (int i = 0; i < num_sequence; i++) {
sample_results[i].push_back(additional_sample_result[i]);
}
RECORD_EVENT(trace_recorder_, request_ids, "finish draft verification");
return {sample_results, last_accepted_tree_node};
}
private:
std::vector<SampleResult> BatchSampleTokensImpl(Tensor probs_on_device, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg, //
const std::vector<RandomGenerator*>& rngs, //
bool top_p_applied) {
// probs_on_device: (n, v)
RECORD_EVENT(trace_recorder_, request_ids, "start sampling");
TVM_FFI_ICHECK_EQ(probs_on_device->ndim, 2);
TVM_FFI_ICHECK_EQ(probs_on_device->device.device_id, device_.device_id);
TVM_FFI_ICHECK_EQ(probs_on_device->device.device_type, device_.device_type);
int num_samples = sample_indices.size();
int num_probs = probs_on_device->shape[0];
int vocab_size = probs_on_device->shape[1];
if (num_samples == 0) {
// This synchronization is necessary for making sure that this round
// of model forward is finished.
DeviceAPI::Get(device_)->StreamSync(device_, compute_stream_);
return {};
}
TVM_FFI_ICHECK_EQ(request_ids.size(), num_samples);
TVM_FFI_ICHECK_EQ(generation_cfg.size(), num_samples);
TVM_FFI_ICHECK_EQ(rngs.size(), num_samples);
// Since `num_samples` may be larger than `max_num_sample_` in some cases,
// we apply chunking to support large `num_samples`.
std::vector<SampleResult> sample_results;
if (num_samples <= max_num_sample_) {
sample_results = ChunkSampleTokensImpl(probs_on_device, sample_indices, generation_cfg, rngs,
top_p_applied);
} else {
for (int chunk_start = 0; chunk_start < num_samples; chunk_start += max_num_sample_) {
int chunk_end = std::min(chunk_start + max_num_sample_, num_samples);
std::vector<int> sample_indices_chunk(sample_indices.begin() + chunk_start,
sample_indices.begin() + chunk_end);
Array<GenerationConfig> generation_cfg_chunk(generation_cfg.begin() + chunk_start,
generation_cfg.begin() + chunk_end);
std::vector<RandomGenerator*> rngs_chunk(rngs.begin() + chunk_start,
rngs.begin() + chunk_end);
std::vector<SampleResult> sample_results_chunk = ChunkSampleTokensImpl(
probs_on_device, sample_indices_chunk, generation_cfg_chunk, rngs_chunk, top_p_applied);
sample_results.insert(sample_results.end(), sample_results_chunk.begin(),
sample_results_chunk.end());
}
}
RECORD_EVENT(trace_recorder_, request_ids, "finish sampling");
return sample_results;
}
/*! \brief Collect the sampling results from the computed Tensor results. */
std::vector<SampleResult> CollectSampleResult(const std::vector<Tensor>& host_arrays,
int num_samples, bool need_prob_values,
const std::vector<int> top_prob_offset_indptr) {
const int* p_sampled_token_ids = static_cast<const int*>(host_arrays[0]->data);
const float* p_sampled_probs = nullptr;
const float* p_top_prob_probs = nullptr;
const int* p_top_prob_indices = nullptr;
if (need_prob_values) {
p_sampled_probs = static_cast<const float*>(host_arrays[1]->data);
p_top_prob_probs = static_cast<const float*>(host_arrays[2]->data);
p_top_prob_indices = static_cast<const int*>(host_arrays[3]->data);
}
std::vector<SampleResult> sample_results;
sample_results.reserve(num_samples);
TVM_FFI_ICHECK_EQ(top_prob_offset_indptr.size(), num_samples + 1);
for (int i = 0; i < num_samples; ++i) {
// Note: we set the probability in SampleResult to 1.0 since prob value is not needed.
float sampled_prob = need_prob_values ? p_sampled_probs[i] : 1.0;
std::vector<TokenProbPair> top_prob_tokens;
top_prob_tokens.reserve(top_prob_offset_indptr[i + 1] - top_prob_offset_indptr[i]);
for (int j = top_prob_offset_indptr[i]; j < top_prob_offset_indptr[i + 1]; ++j) {
top_prob_tokens.emplace_back(p_top_prob_indices[j], p_top_prob_probs[j]);
}
sample_results.push_back(
SampleResult{{p_sampled_token_ids[i], sampled_prob}, top_prob_tokens});
}
return sample_results;
}
std::vector<SampleResult> ChunkSampleTokensImpl(Tensor probs_on_device, //
const std::vector<int>& sample_indices, //
const Array<GenerationConfig>& generation_cfg, //
const std::vector<RandomGenerator*>& rngs, //
bool top_p_applied) {
// probs_on_device: (n, v)
int num_samples = sample_indices.size();
int num_probs = probs_on_device->shape[0];
int vocab_size = probs_on_device->shape[1];
// - Generate random numbers.
// Copy the random numbers and sample indices.
auto uniform_samples_device = GenerateUniformSamples(rngs, num_samples);
auto sample_indices_device = CopySampleIndicesToGPU(sample_indices);
// - Check if there is need for applying top p or prob values,
// so that argsort is needed.
bool need_top_p = false;
if (!top_p_applied) {
need_top_p = CheckTopP(generation_cfg, sample_indices, num_probs, num_samples, vocab_size);
}
// The indptr array of the number of top probs for each sample.
std::vector<int> top_prob_offset_indptr;
bool need_prob_values = CheckProbValues(generation_cfg, sample_indices, num_probs, num_samples,
vocab_size, &top_prob_offset_indptr);
// - Sample tokens on GPU, and take out the probability values if needed.
std::vector<Tensor> device_arrays =
SampleOnGPU(probs_on_device, uniform_samples_device, sample_indices_device, need_top_p,
need_prob_values, num_probs, top_prob_offset_indptr);
// - Copy the GPU sampling function results to CPU.
std::vector<Tensor> host_arrays = CopyArraysToCPU(device_arrays, num_samples, need_prob_values,
top_prob_offset_indptr.back());
// - Collect the sampling results.
return CollectSampleResult(host_arrays, num_samples, need_prob_values, top_prob_offset_indptr);
}
/*! \brief Generate num_samples uniform random numbers, and copy them to GPU. */
Tensor GenerateUniformSamples(const std::vector<RandomGenerator*>& rngs, int num_samples) {
float* p_uniform_samples = static_cast<float*>(uniform_samples_host_->data);
for (int i = 0; i < num_samples; ++i) {
p_uniform_samples[i] = rngs[i]->GetRandomNumber();
}
Tensor uniform_samples_host = uniform_samples_host_.CreateView({num_samples}, dtype_f32_);
Tensor uniform_samples_device = uniform_samples_device_.CreateView({num_samples}, dtype_f32_);
CopyArray(/*src=*/uniform_samples_host, /*dst=*/uniform_samples_device, copy_stream_);
return uniform_samples_device;
}
/*! \brief Generate uniform random numbers, and copy the numbers and sample indices to GPU. The
* number of samples for each random generator is given by `cum_num_samples`. */
Tensor GenerateUniformSamples(const std::vector<RandomGenerator*>& rngs,
const std::vector<int>& cum_num_samples) {
float* p_uniform_samples = static_cast<float*>(uniform_samples_host_->data);
int total_samples = cum_num_samples.back();
for (int i = 0; i + 1 < static_cast<int>(cum_num_samples.size()); ++i) {
for (int j = cum_num_samples[i]; j < cum_num_samples[i + 1]; ++j) {
p_uniform_samples[j] = rngs[i]->GetRandomNumber();
}
}
Tensor uniform_samples_host = uniform_samples_host_.CreateView({total_samples}, dtype_f32_);
Tensor uniform_samples_device = uniform_samples_device_.CreateView({total_samples}, dtype_f32_);
CopyArray(/*src=*/uniform_samples_host, /*dst=*/uniform_samples_device, copy_stream_);
return uniform_samples_device;
}
/*! \brief Generate uniform random numbers, and copy the numbers and sample indices to GPU. */
Tensor CopySampleIndicesToGPU(const std::vector<int>& sample_indices) {
int* p_sample_indices = static_cast<int*>(sample_indices_host_->data);
std::copy(sample_indices.begin(), sample_indices.end(), p_sample_indices);
// Copy the sample indices to GPU.
int num_samples = static_cast<int>(sample_indices.size());
Tensor sample_indices_host = sample_indices_host_.CreateView({num_samples}, dtype_i32_);
Tensor sample_indices_device = sample_indices_device_.CreateView({num_samples}, dtype_i32_);
CopyArray(/*src=*/sample_indices_host, /*dst=*/sample_indices_device, copy_stream_);
return sample_indices_device;
}
/*! \brief Check if top p is needed. Update host top p array in place. */
bool CheckTopP(const Array<GenerationConfig>& generation_cfg,
const std::vector<int>& sample_indices, int num_probs, int num_samples,
int vocab_size) {
// Initialize top p values with -1.
float* p_top_p = static_cast<float*>(top_p_host_->data);
for (int i = 0; i < num_probs; ++i) {
p_top_p[i] = -1.0;
}
bool need_top_p = false;
for (int i = 0; i < num_samples; ++i) {
if (p_top_p[sample_indices[i]] == -1.0) {
p_top_p[sample_indices[i]] = generation_cfg[i]->top_p;
need_top_p |= generation_cfg[i]->top_p != 1.0;
} else {
TVM_FFI_ICHECK(fabs(p_top_p[sample_indices[i]] - generation_cfg[i]->top_p) < eps_)
<< "GPU sampler requires the top_p values for each prob distribution are the same.";
}
}
for (int i = 0; i < num_probs; ++i) {
p_top_p[i] = std::max(p_top_p[i], eps_);
}
return need_top_p;
}
/*! \brief Check whether prob values are needed, and collect info when necessary. */
bool CheckProbValues(const Array<GenerationConfig>& generation_cfg,
const std::vector<int>& sample_indices, int num_probs, int num_samples,
int vocab_size, std::vector<int>* top_prob_offset_indptr) {
top_prob_offset_indptr->reserve(num_samples + 1);
top_prob_offset_indptr->push_back(0);
int* p_top_prob_offsets = static_cast<int*>(top_prob_offsets_host_->data);
int num_top_probs = 0;
bool need_prob_values = false;
for (int i = 0; i < num_samples; ++i) {
need_prob_values |= generation_cfg[i]->logprobs;
for (int j = 0; j < generation_cfg[i]->top_logprobs; ++j) {
p_top_prob_offsets[num_top_probs++] = sample_indices[i] * vocab_size + j;
}
top_prob_offset_indptr->push_back(top_prob_offset_indptr->back() +
generation_cfg[i]->top_logprobs);
}
TVM_FFI_ICHECK_EQ(num_top_probs, top_prob_offset_indptr->back());
return need_prob_values;
}
/*! \brief Sample tokens on GPU. Take out the probability values when needed. */
std::vector<Tensor> SampleOnGPU(Tensor probs_on_device, Tensor uniform_samples_device,
Tensor sample_indices_device, //
bool need_top_p, bool need_prob_values, int num_probs,
const std::vector<int>& top_prob_offset_indptr) {
Tensor sampled_token_ids_device{nullptr};
Tensor sampled_probs_device{nullptr};
Tensor top_prob_probs_device{nullptr};
Tensor top_prob_indices_device{nullptr};
if (!need_top_p && !need_prob_values) {
// - Short path: If top_p and prob values are not needed, we directly sample from multinomial.
SyncCopyStream(device_, compute_stream_, copy_stream_);
if (flashinfer_sampling_available_) {
sampled_token_ids_device =
sampled_token_ids_device_.CreateView({sample_indices_device->shape[0]}, dtype_i32_);
flashinfer_multinomial_sample_func_.value()(probs_on_device, uniform_samples_device,
sample_indices_device,
sampled_token_ids_device);
} else {
sampled_token_ids_device =
gpu_multinomial_from_uniform_func_(probs_on_device, uniform_samples_device,
sample_indices_device)
.cast<Tensor>();
}
return {sampled_token_ids_device, sampled_probs_device, top_prob_probs_device,
top_prob_indices_device};
}
// - Argsort the probability.
Array<Tensor> argsort_results = gpu_argsort_probs_func_(probs_on_device).cast<Array<Tensor>>();
TVM_FFI_ICHECK_EQ(argsort_results.size(), 2);
Tensor sorted_probs_on_device = argsort_results[0];
Tensor sorted_indices_on_device = argsort_results[1];
// - Copy auxiliary array for top-p and prob values in ahead.
Tensor top_p_device;
Tensor top_prob_offsets_device;
if (need_top_p) {
Tensor top_p_host = top_p_host_.CreateView({num_probs}, dtype_f32_);
top_p_device = top_p_device_.CreateView({num_probs}, dtype_f32_);
CopyArray(/*src=*/top_p_host, /*dst=*/top_p_device, copy_stream_);
}
if (need_prob_values) {
int num_top_probs = top_prob_offset_indptr.back();
Tensor top_prob_offsets_host = top_prob_offsets_host_.CreateView({num_top_probs}, dtype_i32_);
top_prob_offsets_device = top_prob_offsets_device_.CreateView({num_top_probs}, dtype_i32_);
CopyArray(/*src=*/top_prob_offsets_host, /*dst=*/top_prob_offsets_device, copy_stream_);
}
SyncCopyStream(device_, compute_stream_, copy_stream_);
if (need_top_p) {
// - Sample with top_p applied.
sampled_token_ids_device =
gpu_sample_with_top_p_func_(sorted_probs_on_device, sorted_indices_on_device,
uniform_samples_device, sample_indices_device, top_p_device)
.cast<Tensor>();
} else {
// - Sample without top_p.
if (flashinfer_sampling_available_) {
sampled_token_ids_device =
sampled_token_ids_device_.CreateView({sample_indices_device->shape[0]}, dtype_i32_);
flashinfer_multinomial_sample_func_
.value()(probs_on_device, uniform_samples_device, sample_indices_device,
sampled_token_ids_device)
.cast<Tensor>();
} else {
sampled_token_ids_device =
gpu_multinomial_from_uniform_func_(probs_on_device, uniform_samples_device,
sample_indices_device)
.cast<Tensor>();
}
}
if (need_prob_values) {
// - Take the probability values.
Array<Tensor> prob_value_results =
gpu_sampler_take_probs_func_(probs_on_device, sorted_indices_on_device,
sample_indices_device, sampled_token_ids_device,
top_prob_offsets_device)
.cast<Array<Tensor>>();
sampled_probs_device = prob_value_results[0];
top_prob_probs_device = prob_value_results[1];
top_prob_indices_device = prob_value_results[2];
}
return {sampled_token_ids_device, sampled_probs_device, top_prob_probs_device,
top_prob_indices_device};
}
/*! \brief Copy the results of GPU sampling functions back to CPU. */
std::vector<Tensor> CopyArraysToCPU(const std::vector<Tensor>& device_arrays, //
int num_samples, bool need_prob_values, int num_top_probs) {
Tensor sampled_token_ids_device = device_arrays[0];
Tensor sampled_probs_device = device_arrays[1];
Tensor top_prob_probs_device = device_arrays[2];
Tensor top_prob_indices_device = device_arrays[3];
TVM_FFI_ICHECK(sampled_token_ids_device.defined());
TVM_FFI_ICHECK_EQ(sampled_token_ids_device->ndim, 1);
TVM_FFI_ICHECK_EQ(sampled_token_ids_device->shape[0], num_samples);
Tensor sampled_token_ids_host = sampled_token_ids_host_.CreateView({num_samples}, dtype_i32_);
CopyArray(/*src=*/sampled_token_ids_device, /*dst=*/sampled_token_ids_host, compute_stream_);
Tensor sampled_probs_host{nullptr};
Tensor top_prob_probs_host{nullptr};
Tensor top_prob_indices_host{nullptr};
if (need_prob_values) {
TVM_FFI_ICHECK(sampled_probs_device.defined());
TVM_FFI_ICHECK(top_prob_probs_device.defined());
TVM_FFI_ICHECK(top_prob_indices_device.defined());
TVM_FFI_ICHECK_EQ(sampled_probs_device->ndim, 1);
TVM_FFI_ICHECK_EQ(top_prob_probs_device->ndim, 1);
TVM_FFI_ICHECK_EQ(top_prob_indices_device->ndim, 1);
TVM_FFI_ICHECK_EQ(sampled_probs_device->shape[0], num_samples);
TVM_FFI_ICHECK_EQ(top_prob_probs_device->shape[0], num_top_probs);
TVM_FFI_ICHECK_EQ(top_prob_indices_device->shape[0], num_top_probs);
sampled_probs_host = sampled_probs_host_.CreateView({num_samples}, dtype_i32_);
top_prob_probs_host = top_prob_probs_host_.CreateView({num_top_probs}, dtype_f32_);
top_prob_indices_host = top_prob_indices_host_.CreateView({num_top_probs}, dtype_i32_);
CopyArray(/*src=*/sampled_probs_device, /*dst=*/sampled_probs_host, compute_stream_);
if (num_top_probs > 0) {
CopyArray(/*src=*/top_prob_probs_device, /*dst=*/top_prob_probs_host, compute_stream_);
CopyArray(/*src=*/top_prob_indices_device, /*dst=*/top_prob_indices_host, compute_stream_);
}
}
// Synchronize for CPU to get the correct array results.
DeviceAPI::Get(device_)->StreamSync(device_, compute_stream_);
return {sampled_token_ids_host, sampled_probs_host, top_prob_probs_host, top_prob_indices_host};
}
// Model configurations
const int max_num_sample_;
const int vocab_size_;
const DLDataType dtype_i32_ = DLDataType{kDLInt, 32, 1};
const DLDataType dtype_f32_ = DLDataType{kDLFloat, 32, 1};
const bool flashinfer_sampling_available_;
// Functions for sampling on GPU.
Device device_;
Function gpu_multinomial_from_uniform_func_;
Function gpu_argsort_probs_func_;
Function gpu_sample_with_top_p_func_;
Function gpu_sampler_take_probs_func_;
Function gpu_verify_draft_tokens_func_;
Function gpu_renormalize_by_top_p_func_;
Optional<Function> flashinfer_multinomial_sample_func_;
// Auxiliary Tensors on CPU
Tensor uniform_samples_host_;
Tensor sample_indices_host_;
Tensor top_p_host_;
Tensor top_p_init_pivots_host_;
Tensor top_prob_offsets_host_;
Tensor draft_tokens_host_;
Tensor token_tree_first_child_host_;
Tensor token_tree_next_sibling_host_;
Tensor token_tree_parent_ptr_host_;
Tensor sampled_token_ids_host_;
Tensor sampled_probs_host_;
Tensor top_prob_probs_host_;
Tensor top_prob_indices_host_;
// Auxiliary Tensors on GPU
Tensor uniform_samples_device_;
Tensor sample_indices_device_;
Tensor top_p_device_;
Tensor top_p_init_pivots_device_;
Tensor top_prob_offsets_device_;
Tensor draft_tokens_device_;
Tensor token_tree_first_child_device_;
Tensor token_tree_next_sibling_device_;
Tensor token_tree_parent_ptr_device_;
Tensor sampled_token_ids_device_;
// The event trace recorder for requests. */
Optional<EventTraceRecorder> trace_recorder_;
// The device stream for the default computation operations.
TVMStreamHandle compute_stream_ = nullptr;
// The device stream for copying auxiliary data structure to GPU.
TVMStreamHandle copy_stream_ = nullptr;
const float eps_ = 1e-5;
const int num_top_p_cutoff_pivots_ = 3;
};
Sampler Sampler::CreateGPUSampler(int max_num_sample, int vocab_size, FunctionTable* ft,
DLDevice device, Optional<EventTraceRecorder> trace_recorder) {
return Sampler(tvm::ffi::make_object<GPUSampler>(max_num_sample, vocab_size, ft, device,
std::move(trace_recorder)));
}
} // namespace serve
} // namespace llm
} // namespace mlc
+165
View File
@@ -0,0 +1,165 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/sampler/sampler.h
* \brief The header for runtime module of sampler functions.
*/
#ifndef MLC_LLM_SERVE_SAMPLER_SAMPLER_H_
#define MLC_LLM_SERVE_SAMPLER_SAMPLER_H_
#include <tvm/ffi/extra/module.h>
#include <tvm/ffi/string.h>
#include "../../base.h"
#include "../../support/random.h"
#include "../data.h"
#include "../event_trace_recorder.h"
#include "../model.h"
#include "../request_state.h"
namespace mlc {
namespace llm {
namespace serve {
using tvm::Device;
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
/*!
* \brief The base class of runtime sampler.
* Its main function is `BatchSampleTokensWithProbBeforeTopP`, which takes a batch of
* logits and corresponding configuration, and sample one token
* for each instance of the batch.
*/
class SamplerObj : public Object {
public:
/*!
* \brief Renormalize the input batch of probability distributions with top p values.
* \param probs_on_device The batch of prob distributions before normalization.
* \param sample_indices Specifying which request we will sample for
* in i-th output for the sampling later on.
* The output result of the sampling will be as follow:
* result[i] = sample_from(prob_on_device[sample_indices[i],:], generation_config[i]));
* For renormalization, the sample indices are used for determine the top-p grouping.
* \param request_ids The id of each request.
* \param generation_cfg The generation config of each request in the input batch.
* \return The renormalized probability distributions, residing on device
* if the sampler is GPU sampler, or on host if the sampler is CPU sampler.
*/
virtual Tensor BatchRenormalizeProbsByTopP(Tensor probs_on_device, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg) = 0;
/*!
* \brief Sample tokens from the input batch of prob distribution on device.
* The input prob distributions are not yet applied with top-p.
* \param probs_on_device The prob distributions on GPU to sample tokens from.
* \param sample_indices Specifying which request we should sample for
* in i-th output. The output result is sample as follow:
* result[i] = sample_from(prob_on_device[sample_indices[i],:], generation_config[i]));
* \param request_ids The id of each request.
* \param generation_cfg The generation config of each request
* in the input batch.
* \param rngs The random number generator of each sequence.
* \return The batch of sampling results, which contain the sampled token id
* and other probability info.
*/
virtual std::vector<SampleResult> BatchSampleTokensWithProbBeforeTopP(
Tensor probs_on_device, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg, //
const std::vector<RandomGenerator*>& rngs) = 0;
/*!
* \brief Sample tokens from the input batch of prob distribution on device.
* The input prob distributions are already applied with top-p.
* \param probs The prob distributions.
* It resides on GPU if the sampler is GPU sampler, or on host if hte sampler is CPU sampler.
* \param sample_indices Specifying which request we should sample for
* in i-th output. The output result is sample as follow:
* result[i] = sample_from(prob_on_device[sample_indices[i],:], generation_config[i]));
* \param request_ids The id of each request.
* \param generation_cfg The generation config of each request
* in the input batch.
* \param rngs The random number generator of each sequence.
* \return The batch of sampling results, which contain the sampled token id
* and other probability info.
*/
virtual std::vector<SampleResult> BatchSampleTokensWithProbAfterTopP(
Tensor probs, //
const std::vector<int>& sample_indices, //
const Array<String>& request_ids, //
const Array<GenerationConfig>& generation_cfg, //
const std::vector<RandomGenerator*>& rngs) = 0;
/*!
* \brief Verify draft tokens generated by small models in the large model
* in speculative decoding. The input corresponds to a batch of sequences.
* The input prob distributions are already applied with top-p.
* \param probs The prob distributions on GPU to sample tokens from.
* It resides on GPU if the sampler is GPU sampler, or on host if hte sampler is CPU sampler.
* \param request_ids The id of each request.
* \param cum_verify_lengths The cumulative draft lengths to verify of all sequences.
* \param generation_cfg The generation config of each request
* in the input batch.
* \param rngs The random number generator of each sequence.
* \param draft_output_tokens The draft tokens generated by the small model for
* each sequence.
* \param token_tree_parent_ptr The parent pointer of the token tree.
* \param draft_probs_on_device The probability distribution computed from the
* small model for each sequence. Concatenated tensor of shape (total_verify_length, vocab_size).
* It includes the slot for the last committed token that has undefined probablity value.
* \return The list of accepted tokens for each request and the index of the last accepted tree
* node for each request.
*/
virtual std::pair<std::vector<std::vector<SampleResult>>, std::vector<int>>
BatchVerifyDraftTokensWithProbAfterTopP(
Tensor probs, const Array<String>& request_ids, const std::vector<int>& cum_verify_lengths,
const Array<GenerationConfig>& generation_cfg, const std::vector<RandomGenerator*>& rngs,
const std::vector<std::vector<SampleResult>>& draft_output_tokens,
const std::vector<int64_t>& token_tree_parent_ptr, Tensor draft_probs_on_device) = 0;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<SamplerObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.Sampler", SamplerObj, Object);
};
class Sampler : public ObjectRef {
public:
/*! * \brief Create a CPU sampler. */
static Sampler CreateCPUSampler(Optional<EventTraceRecorder> trace_recorder);
/*!
* \brief Create a GPU sampler.
* \param max_num_sample The max number of samples to sample at a time.
* \param vocab_size The model's vocabulary size.
* \param ft The packed function table.
* \param device The device that the model runs on.
* \param trace_recorder The event trace recorder.
*/
static Sampler CreateGPUSampler(int max_num_sample, int vocab_size, FunctionTable* ft,
DLDevice device, Optional<EventTraceRecorder> trace_recorder);
/*! \brief Check if the given device supports GPU sampling. */
static bool SupportGPUSampler(Device device) {
return device.device_type == DLDeviceType::kDLCUDA ||
device.device_type == DLDeviceType::kDLVulkan ||
device.device_type == DLDeviceType::kDLMetal;
}
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(Sampler, ObjectRef, SamplerObj);
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_SAMPLER_SAMPLER_H_
+417
View File
@@ -0,0 +1,417 @@
/*!
* 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
+90
View File
@@ -0,0 +1,90 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file serve/threaded_engine.h
* \brief The header of threaded serving engine in MLC LLM.
*/
#ifndef MLC_LLM_SERVE_THREADED_ENGINE_H_
#define MLC_LLM_SERVE_THREADED_ENGINE_H_
#include "data.h"
#include "engine.h"
#include "request.h"
namespace mlc {
namespace llm {
namespace serve {
using namespace tvm::runtime;
/*!
* \brief The interface threaded engine in MLC LLM.
* The threaded engine keeps running a background request processing
* loop on a standalone thread. Ensuring thread safety, it exposes
* `AddRequest` and `AbortRequest` to receive new requests or
* abortions from other threads, and the internal request processing
* is backed by a normal engine wrapped inside.
*/
class ThreadedEngine {
public:
/*! \brief Create a ThreadedEngine. */
static std::unique_ptr<ThreadedEngine> Create();
virtual ~ThreadedEngine() = default;
/*!
* \brief Initialize the threaded engine from packed arguments in PackedArgs.
* \param device The device where to run models.
* \param request_stream_callback The request stream callback function to.
* \param trace_recorder Event trace recorder for requests.
*/
virtual void InitThreadedEngine(Device device, Optional<Function> request_stream_callback,
Optional<EventTraceRecorder> trace_recorder) = 0;
/*!
* \brief Reload the engine with the new engine config.
* \param engine_config_json_str The engine config JSON string.
*/
virtual void Reload(String engine_config_json_str) = 0;
/*! \brief Unload the background engine. */
virtual void Unload() = 0;
/*! \brief Reset the engine to the initial state. */
virtual void Reset() = 0;
/*! \brief Starts the background request processing loop. */
virtual void RunBackgroundLoop() = 0;
/*! \brief Starts the request stream callback loop. */
virtual void RunBackgroundStreamBackLoop() = 0;
/*!
* \brief Notify the ThreadedEngine to exit the background
* request processing loop. This method is invoked by threads
* other than the engine-driving thread.
*/
virtual void ExitBackgroundLoop() = 0;
/*! \brief Add a new request to the engine. */
virtual void AddRequest(Request request) = 0;
/*! \brief Abort the input request (specified by id string) from engine. */
virtual void AbortRequest(const String& request_id) = 0;
/************** Query/Profile/Debug **************/
/*! \brief Return the default generation config. */
virtual GenerationConfig GetDefaultGenerationConfig() const = 0;
/*! \brief Return the complete engine config. */
virtual EngineConfig GetCompleteEngineConfig() const = 0;
/*! \brief Call the given global function on all workers. Only for debug purpose. */
virtual void DebugCallFuncOnAllAllWorker(const String& func_name, Optional<String> func_args) = 0;
};
} // namespace serve
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SERVE_THREADED_ENGINE_H_
+37
View File
@@ -0,0 +1,37 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/debug_utils.h
* \brief Tools for debug purposes.
*/
#ifndef MLC_LLM_SUPPORT_DEBUG_UTILS_H_
#define MLC_LLM_SUPPORT_DEBUG_UTILS_H_
#include "../tokenizers/tokenizers.h"
namespace mlc {
namespace llm {
/*! \brief A registry for debug information. */
class DebugRegistry {
public:
static DebugRegistry* Global() {
static DebugRegistry reg;
return &reg;
}
// Tokenizer information, helpful for converting token id to token string in debugging
Tokenizer tokenizer;
};
/*! \brief Register the tokenizer to the global tokenizer registry. */
inline void DebugRegisterTokenizer(const Tokenizer& tokenizer) {
DebugRegistry::Global()->tokenizer = tokenizer;
}
/*! \brief Get the registered tokenizer from the global tokenizer registry. */
inline Tokenizer DebugGetTokenizer() { return DebugRegistry::Global()->tokenizer; }
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_DEBUG_UTILS_H_
+147
View File
@@ -0,0 +1,147 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/dynamic_bitset.h
* \brief The header for utilities used in grammar-guided generation.
*/
#ifndef MLC_LLM_SUPPORT_DYNAMIC_BITSET_H_
#define MLC_LLM_SUPPORT_DYNAMIC_BITSET_H_
#include <tvm/runtime/logging.h>
#include <cstdint>
#include <cstring>
#include <vector>
namespace mlc {
namespace llm {
/*!
* \brief A bitset whose length is specified at runtime. Note the size cannot be changed after
* construction.
* \details The buffer of the bitset is a uint32_t array. There are two uses for this class:
* - When passing nullptr to data, it maintains an internal buffer for the bitset.
* - When passing a pointer to a buffer with enough size, it uses the external buffer for the
* bitset.
*/
class DynamicBitset {
public:
/*!
* \brief Calculate the minimal size of the uint32_t buffer for the bitset with the given size.
* \param element_size The size of the bitset.
* \return The minimal buffer size.
*/
static int CalculateBufferSize(int element_size) { return (element_size + 31) / 32; }
/*!
* \brief Construct a empty bitset. This object should be assigned to a valid bitset before using.
*/
DynamicBitset() : size_(0), buffer_size_(0), data_(nullptr), is_internal_(true) {}
/*!
* \brief Construct a bitset with the given size.
* \param size The size of the bitset.
* \param data The buffer for the bitset. If nullptr, the bitset will maintain an internal buffer.
*/
DynamicBitset(int size, uint32_t* data = nullptr)
: size_(size), buffer_size_(CalculateBufferSize(size)) {
if (data == nullptr) {
internal_buffer_.resize(buffer_size_, 0);
data_ = internal_buffer_.data();
is_internal_ = true;
} else {
data_ = data;
is_internal_ = false;
}
}
/*! \brief Copy assignment. */
DynamicBitset& operator=(const DynamicBitset& other) {
TVM_FFI_DCHECK(is_internal_ || size_ >= other.size_)
<< "Expanding bitset size is not allowed when the "
"memory of the bitset is externally managed";
size_ = other.size_;
buffer_size_ = other.buffer_size_;
if (is_internal_) {
internal_buffer_.reserve(buffer_size_);
data_ = internal_buffer_.data();
}
if (data_ != other.data_) {
std::memcpy(data_, other.data_, buffer_size_ * sizeof(uint32_t));
}
return *this;
}
/*! \brief Move assignment. */
DynamicBitset& operator=(DynamicBitset&& other) {
size_ = other.size_;
buffer_size_ = other.buffer_size_;
is_internal_ = other.is_internal_;
if (is_internal_) {
internal_buffer_ = std::move(other.internal_buffer_);
data_ = internal_buffer_.data();
} else {
data_ = other.data_;
}
return *this;
}
/*! \brief Get the value of the bit at the given index. */
bool operator[](int index) const {
TVM_FFI_DCHECK(data_ && index >= 0 && index < size_);
return (data_[index / 32] >> (index % 32)) & 1;
}
/*! \brief Get the size of the bitset. */
int Size() const { return size_; }
/*! \brief Set the whole bitset to true. */
void Set() {
TVM_FFI_DCHECK(data_);
std::memset(data_, 0xFF, buffer_size_ * sizeof(uint32_t));
}
/*! \brief Set the bit at the given index to the given value. */
void Set(int index, bool value = true) {
TVM_FFI_DCHECK(data_ && index >= 0 && index < size_);
if (value) {
data_[index / 32] |= 1 << (index % 32);
} else {
data_[index / 32] &= ~(1 << (index % 32));
}
}
/*! \brief Set the whole bitset to false. */
void Reset() {
TVM_FFI_DCHECK(data_);
std::memset(data_, 0, buffer_size_ * sizeof(uint32_t));
}
/*! \brief Set the bit at the given index to false. */
void Reset(int index) { Set(index, false); }
/*! \brief Perform a bitwise OR operation between the current bitset and another bitset. */
DynamicBitset& operator|=(const DynamicBitset& other) {
TVM_FFI_DCHECK(buffer_size_ <= other.buffer_size_);
for (int i = 0; i < buffer_size_; ++i) {
data_[i] |= other.data_[i];
}
return *this;
}
private:
// The size of the bitset.
int size_;
// The size of the buffer.
int buffer_size_;
// The buffer for the bitset.
uint32_t* data_;
// The internal buffer. It is empty if not needed.
std::vector<uint32_t> internal_buffer_;
// Whether the buffer is internally managed.
bool is_internal_;
};
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_DYNAMIC_BITSET_H_
+211
View File
@@ -0,0 +1,211 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/encoding.cc
*/
#include "encoding.h"
#include <tvm/runtime/logging.h>
#include <array>
namespace mlc {
namespace llm {
std::string PrintAsUTF8(TCodepoint codepoint) {
TVM_FFI_ICHECK(codepoint <= 0x10FFFF) << "Invalid codepoint: " << codepoint;
std::string utf8;
if (codepoint <= 0x7F) {
// 1-byte sequence
utf8 += static_cast<char>(codepoint);
} else if (codepoint <= 0x7FF) {
// 2-byte sequence
utf8 += static_cast<char>(0xC0 | ((codepoint >> 6) & 0x1F));
utf8 += static_cast<char>(0x80 | (codepoint & 0x3F));
} else if (codepoint <= 0xFFFF) {
// 3-byte sequence
utf8 += static_cast<char>(0xE0 | ((codepoint >> 12) & 0x0F));
utf8 += static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F));
utf8 += static_cast<char>(0x80 | (codepoint & 0x3F));
} else {
// 4-byte sequence
utf8 += static_cast<char>(0xF0 | ((codepoint >> 18) & 0x07));
utf8 += static_cast<char>(0x80 | ((codepoint >> 12) & 0x3F));
utf8 += static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F));
utf8 += static_cast<char>(0x80 | (codepoint & 0x3F));
}
return utf8;
}
std::string PrintAsEscaped(
TCodepoint codepoint,
const std::unordered_map<TCodepoint, std::string>& additional_escape_map) {
static const std::unordered_map<TCodepoint, std::string> kCodepointToEscape = {
{'\'', "\\\'"}, {'\"', "\\\""}, {'\?', "\\\?"}, {'\\', "\\\\"}, {'\a', "\\a"},
{'\b', "\\b"}, {'\f', "\\f"}, {'\n', "\\n"}, {'\r', "\\r"}, {'\t', "\\t"},
{'\v', "\\v"}, {'\0', "\\0"}, {'\x1B', "\\e"}};
if (auto it = additional_escape_map.find(codepoint); it != additional_escape_map.end()) {
return it->second;
}
if (auto it = kCodepointToEscape.find(codepoint); it != kCodepointToEscape.end()) {
return it->second;
}
if (codepoint >= 0x20 && codepoint <= 0x7E) {
return std::string({static_cast<char>(codepoint)});
}
// convert codepoint to hex
char prefix = codepoint <= 0xFF ? 'x' : codepoint <= 0xFFFF ? 'u' : 'U';
int width = codepoint <= 0xFF ? 2 : codepoint <= 0xFFFF ? 4 : 8;
std::stringstream ss;
ss << std::setfill('0') << std::setw(width) << std::hex << codepoint;
auto hex = ss.str();
return std::string("\\") + prefix + hex;
}
std::string PrintAsEscaped(uint8_t raw_char) { return PrintAsEscaped(raw_char); }
std::string PrintAsEscaped(std::string raw_str) {
std::string res;
auto codepoints = ParseUTF8(raw_str.c_str(), UTF8ErrorPolicy::kReturnByte);
for (auto c : codepoints) {
res += PrintAsEscaped(c);
}
return res;
}
std::tuple<bool, int, TCodepoint> HandleUTF8FirstByte(uint8_t byte) {
static const std::array<int8_t, 5> kFirstByteMask = {0x00, 0x7F, 0x1F, 0x0F, 0x07};
// clang-format off
static const std::array<int, 256> kUtf8Bytes = {
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
4, 4, 4, 4, 4, 4, 4, 4, -1, -1, -1, -1, -1, -1, -1, -1,
};
// clang-format on
auto num_bytes = kUtf8Bytes[static_cast<uint8_t>(byte)];
if (num_bytes == -1) {
return {false, 0, 0};
}
return {true, num_bytes, byte & kFirstByteMask[num_bytes]};
}
std::pair<TCodepoint, const char*> ParseNextUTF8(const char* utf8, UTF8ErrorPolicy error_policy) {
auto [accepted, num_bytes, res] = HandleUTF8FirstByte(utf8[0]);
if (accepted) {
for (int i = 1; i < num_bytes; ++i) {
if (utf8[i] == 0 || (static_cast<uint8_t>(utf8[i]) & 0xC0) != 0x80) {
// invalid utf8
accepted = false;
break;
}
res = (res << 6) | (static_cast<uint8_t>(utf8[i]) & 0x3F);
}
}
if (!accepted) {
// invalid utf8
if (error_policy == UTF8ErrorPolicy::kReturnInvalid) {
return {CharHandlingError::kInvalidUTF8, utf8};
} else {
return {static_cast<unsigned char>(utf8[0]), utf8 + 1};
}
}
return {res, utf8 + num_bytes};
}
std::vector<TCodepoint> ParseUTF8(const char* utf8, UTF8ErrorPolicy error_policy) {
std::vector<TCodepoint> codepoints;
while (*utf8 != 0) {
TCodepoint codepoint;
std::tie(codepoint, utf8) = ParseNextUTF8(utf8, error_policy);
if (codepoint == CharHandlingError::kInvalidUTF8) {
return {codepoint};
}
codepoints.push_back(codepoint);
}
return codepoints;
}
inline int HexCharToInt(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
} else if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
} else if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
} else {
return -1;
}
}
std::pair<TCodepoint, const char*> ParseNextUTF8OrEscaped(
const char* utf8, const std::unordered_map<std::string, TCodepoint>& additional_escape_map) {
static const std::unordered_map<std::string, TCodepoint> kEscapeToCodepoint = {
{"\\\'", '\''}, {"\\\"", '\"'}, {"\\\?", '\?'}, {"\\\\", '\\'}, {"\\a", '\a'},
{"\\b", '\b'}, {"\\f", '\f'}, {"\\n", '\n'}, {"\\r", '\r'}, {"\\t", '\t'},
{"\\v", '\v'}, {"\\0", '\0'}, {"\\e", '\x1B'}};
if (utf8[0] != '\\') {
return ParseNextUTF8(utf8, UTF8ErrorPolicy::kReturnInvalid);
}
auto escape_sequence = std::string(utf8, 2);
if (auto it = additional_escape_map.find(escape_sequence); it != additional_escape_map.end()) {
return {it->second, utf8 + 2};
}
if (auto it = kEscapeToCodepoint.find(escape_sequence); it != kEscapeToCodepoint.end()) {
return {it->second, utf8 + 2};
}
if (utf8[1] == 'x') {
// arbitrary length hex
int len = 0;
int32_t codepoint = 0;
while (true) {
auto digit = HexCharToInt(utf8[2 + len]);
if (digit == -1) {
break;
}
codepoint = codepoint * 16 + digit;
++len;
}
if (len == 0) {
return {CharHandlingError::kInvalidEscape, utf8};
}
return {codepoint, utf8 + len + 2};
} else if (utf8[1] == 'u' || utf8[1] == 'U') {
// 4- or 8-digit hex
int len = utf8[1] == 'u' ? 4 : 8;
int32_t codepoint = 0;
for (int i = 0; i < len; ++i) {
auto digit = HexCharToInt(utf8[i + 2]);
if (digit == -1) {
return {CharHandlingError::kInvalidEscape, utf8};
}
codepoint = codepoint * 16 + digit;
}
return {codepoint, utf8 + len + 2};
} else {
return {CharHandlingError::kInvalidEscape, utf8};
}
}
} // namespace llm
} // namespace mlc
+114
View File
@@ -0,0 +1,114 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/encoding.h
* \brief Encoding and decoding from/to UTF-8 and escape sequence to/from codepoints.
*/
#ifndef MLC_LLM_SUPPORT_ENCODING_H_
#define MLC_LLM_SUPPORT_ENCODING_H_
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace mlc {
namespace llm {
/*! \brief Represents a unicode codepoint. */
using TCodepoint = int32_t;
/*!
* \brief Handle the utf-8 first byte.
* \returns (is_valid, total_number_of_bytes, initial_codepoint).
*/
std::tuple<bool, int, TCodepoint> HandleUTF8FirstByte(uint8_t byte);
/*!
* \brief Print a codepoint to a UTF-8 string.
* \param codepoint The codepoint.
* \return The UTF-8 string.
*/
std::string PrintAsUTF8(TCodepoint codepoint);
/*!
* \brief Print a codepoint to a escaped string. If the codepoint is not printable, it will be
* escaped. By default the function support escape sequences in C ("\n", "\t", "\u0123"). User can
* specify more escape sequences using additional_escape_map.
* \param codepoint The codepoint.
* \param additional_escape_map A map from codepoint to escape sequence. If the codepoint is in the
* map, it will be escaped using the corresponding escape sequence. e.g. {{'-', "\\-"}}. \return The
* printable string.
*/
std::string PrintAsEscaped(
TCodepoint codepoint,
const std::unordered_map<TCodepoint, std::string>& additional_escape_map = {});
/*!
* \brief Print the given char to a escaped string that can be printed.
* \return The escaped string.
*/
std::string PrintAsEscaped(uint8_t raw_char);
/*!
* \brief Print the given string to a escaped string that can be printed.
* \return The escaped string.
*/
std::string PrintAsEscaped(std::string raw_str);
/*!
* \brief Represents an error when handling characters. Will be returned as a special TCodepoint
* value.
*/
enum CharHandlingError : TCodepoint {
/*! \brief The UTF-8 string is invalid. */
kInvalidUTF8 = -10,
/*! \brief The escape sequence is invalid. */
kInvalidEscape = -11,
};
/*!
* \brief The method to handle invalid UTF-8 sequence.
*/
enum class UTF8ErrorPolicy {
/*! \brief Return an error codepoint when an error is encountered. */
kReturnInvalid,
/*! \brief Skip the error and continue parsing. */
kReturnByte,
};
/*!
* \brief Parse the first codepoint in a UTF-8 string.
* \param utf8 The UTF-8 string.
* \return The codepoint and new pointer. If the UTF-8 string is invalid, and the error policy is
* kReturnInvalid, the function returns (CharHandlingError::kInvalidUTF8, input char pointer).
*/
std::pair<TCodepoint, const char*> ParseNextUTF8(
const char* utf8, UTF8ErrorPolicy error_policy = UTF8ErrorPolicy::kReturnInvalid);
/*!
* \brief Parse all codepoints in a UTF-8 string.
* \param utf8 The UTF-8 string.
* \return All codepoints. If the UTF-8 string is invalid, and the error policy is
* kReturnInvalid, the function returns {CharHandlingError::kInvalidUTF8}.
*/
std::vector<TCodepoint> ParseUTF8(const char* utf8,
UTF8ErrorPolicy error_policy = UTF8ErrorPolicy::kReturnInvalid);
/*!
* \brief Parse the first codepoint from a UTF-8 string. Also checks escape sequences and converts
* the escaped char to its original value.
* \param utf8 The UTF-8 string or the escape sequence.
* \param additional_escape_map A map from escape sequence to codepoint. If the escape sequence is
* in the map, it will be converted to the corresponding codepoint. e.g. {{"\\-", '-'}}.
* \return The codepoint and the new pointer. If the UTF-8 string or the escape sequence is
* invalid, and the error policy is kReturnInvalid, the function returns
* (CharHandlingError::kInvalidUTF8, input char pointer).
*/
std::pair<TCodepoint, const char*> ParseNextUTF8OrEscaped(
const char* utf8,
const std::unordered_map<std::string, TCodepoint>& additional_escape_map = {});
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_ENCODING_H_
+284
View File
@@ -0,0 +1,284 @@
/*!
* \file support/json_parser.h
* \brief Helps to parse JSON strings and objects.
*/
#ifndef MLC_LLM_SUPPORT_JSON_PARSER_H_
#define MLC_LLM_SUPPORT_JSON_PARSER_H_
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/dtype.h>
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/string.h>
#include <tvm/runtime/logging.h>
#include <optional>
#include "result.h"
namespace mlc {
namespace llm {
namespace json {
using ::tvm::ffi::json::Array;
using ::tvm::ffi::json::Object;
using ::tvm::ffi::json::Value;
/*!
* \brief Parse a JSON string to a JSON object.
* \param json_str The JSON string to parse.
* \return The parsed JSON object.
*/
inline Object ParseToJSONObject(const std::string& json_str) {
tvm::ffi::String err;
Value result = ::tvm::ffi::json::Parse(json_str, &err);
TVM_FFI_CHECK(err.empty(), ValueError)
<< "Failed to parse JSON: err. The JSON string is:" << json_str;
auto opt = result.try_cast<Object>();
TVM_FFI_CHECK(opt.has_value(), ValueError)
<< "The given string is not a JSON object: " << json_str;
return *opt;
}
/*!
* \brief Parse a JSON string to a JSON object.
* \param json_str The JSON string to parse.
* \return The parsed JSON object, or the error message.
*/
inline Result<Object> ParseToJSONObjectWithResultReturn(const std::string& json_str) {
using TResult = Result<Object>;
tvm::ffi::String err;
Value result = ::tvm::ffi::json::Parse(json_str, &err);
if (!err.empty()) {
return TResult::Error("Failed to parse JSON: err. The JSON string is: " + json_str +
". The error is " + std::string(err));
}
auto opt = result.try_cast<Object>();
if (!opt.has_value()) {
return TResult::Error("ValueError: The given string is not a JSON object: " + json_str);
}
return TResult::Ok(*opt);
}
/*!
* \brief Lookup a JSON object by a key, and convert it to a given type.
* \param json The JSON object to look up.
* \param key The key to look up.
* \tparam ValueType The type to be converted to.
* \return The converted value.
*/
template <typename ValueType>
ValueType Lookup(const Object& json, const std::string& key);
/*!
* \brief Lookup a JSON array by an index, and convert it to a given type.
* \param json The JSON array to look up.
* \param index The index to look up.
* \tparam ValueType The type to be converted to.
* \return The converted value.
*/
template <typename ValueType>
ValueType Lookup(const Array& json, int index);
/*!
* \brief Lookup a JSON object by a key, and convert it to a given type.
* If the key doesn't exist or has null value, the default value is returned.
* \param json The JSON object to look up.
* \param key The key to look up.
* \tparam ValueType The type to be converted to.
* \return The converted value, or the default value if the key doesn't exist or has null value.
*/
template <typename ValueType>
inline ValueType LookupOrDefault(const Object& json, const std::string& key,
const ValueType& default_value) {
if (json.count(key) == 0 || json.at(key) == nullptr) {
return default_value;
}
auto opt = json.at(key).try_cast<ValueType>();
TVM_FFI_CHECK(opt.has_value(), ValueError) << "key `" << key << "` has unexpected type";
return *opt;
}
/*!
* \brief Lookup a JSON object by a key, and convert it to a given type.
* If the key doesn't exist or has null value, return std::nullopt.
* \param json The JSON object to look up.
* \param key The key to look up.
* \tparam ValueType The type to be converted to.
* \return The converted value, or std::nullopt if the value doesn't exist or has null value.
*/
template <typename ValueType>
inline std::optional<ValueType> LookupOptional(const Object& json, const std::string& key) {
if (json.count(key) == 0 || json.at(key) == nullptr) {
return std::nullopt;
}
auto opt = json.at(key).try_cast<ValueType>();
TVM_FFI_CHECK(opt.has_value(), ValueError) << "key `" << key << "` has unexpected type";
return *opt;
}
/*!
* \brief Lookup a JSON object by a key, and convert it to a given type.
* \param json The JSON object to look up.
* \param key The key to look up.
* \tparam ValueType The type to be converted to.
* \return The converted value, or the error message.
*/
template <typename ValueType>
inline Result<ValueType> LookupWithResultReturn(const Object& json, const std::string& key) {
using TResult = Result<ValueType>;
if (json.count(key) == 0) {
return TResult::Error("ValueError: key \"" + key + "\" not found in the JSON object");
}
auto opt = json.at(key).try_cast<ValueType>();
if (!opt.has_value()) {
return TResult::Error("ValueError: key \"" + key + "\" has unexpected value type.");
}
return TResult::Ok(*opt);
}
/*!
* \brief Lookup a JSON object by a key, and convert it to a given type.
* If the key doesn't exist or has null value, the default value is returned.
* \param json The JSON object to look up.
* \param key The key to look up.
* \tparam ValueType The type to be converted to.
* \return The converted value, or the default value if the key doesn't exist or has null value
* , or the error message.
*/
template <typename ValueType>
inline Result<ValueType> LookupOrDefaultWithResultReturn(const Object& json, const std::string& key,
const ValueType& default_value) {
using TResult = Result<ValueType>;
if (json.count(key) == 0 || json.at(key) == nullptr) {
return TResult::Ok(default_value);
}
auto opt = json.at(key).try_cast<ValueType>();
if (!opt.has_value()) {
return TResult::Error("ValueError: key \"" + key + "\" has unexpected value type.");
}
return TResult::Ok(*opt);
}
/*!
* \brief Lookup a JSON object by a key, and convert it to a given type.
* If the key doesn't exist or has null value, return std::nullopt.
* \param json The JSON object to look up.
* \param key The key to look up.
* \tparam ValueType The type to be converted to.
* \return The converted value, or std::nullopt if the value doesn't exist or has null value,
* , or the error message.
*/
template <typename ValueType>
inline Result<std::optional<ValueType>> LookupOptionalWithResultReturn(const Object& json,
const std::string& key) {
using TResult = Result<std::optional<ValueType>>;
if (json.count(key) == 0 || json.at(key) == nullptr) {
return TResult::Ok(std::nullopt);
}
auto opt = json.at(key).try_cast<ValueType>();
if (!opt.has_value()) {
return TResult::Error("ValueError: key \"" + key + "\" has unexpected value type.");
}
return TResult::Ok(*opt);
}
// Implementation details
/*! \brief Shape extension to incorporate symbolic shapes. */
struct SymShapeTuple {
tvm::ffi::Shape shape_values;
std::vector<std::string> sym_names;
/*! \brief Convert symbolic shape tuple to static shape tuple with model config. */
tvm::ffi::Shape ToStatic(const Object& model_config) {
std::vector<int64_t> shape;
shape.reserve(shape_values.size());
for (int i = 0; i < static_cast<int>(shape_values.size()); ++i) {
if (shape_values[i] != -1) {
shape.push_back(shape_values[i]);
} else {
auto opt = model_config.at(sym_names[i]).try_cast<int64_t>();
TVM_FFI_CHECK(opt.has_value(), ValueError)
<< "model config is expected to contain \"" << sym_names[i]
<< "\" as an integer. However, the given config has unexpected type for \""
<< sym_names[i] << "\".";
shape.push_back(*opt);
}
}
return tvm::ffi::Shape(std::move(shape));
}
};
namespace details {
inline DLDataType DTypeFromString(const std::string& s) { return tvm::ffi::StringToDLDataType(s); }
inline SymShapeTuple SymShapeTupleFromArray(const Array& shape) {
std::vector<int64_t> result;
std::vector<std::string> sym_names;
result.reserve(shape.size());
sym_names.reserve(shape.size());
for (int i = 0; i < static_cast<int>(shape.size()); ++i) {
const auto& dim = shape[i];
auto str_opt = dim.try_cast<std::string>();
if (str_opt.has_value()) {
result.push_back(-1);
sym_names.push_back(*str_opt);
} else {
auto int_opt = dim.try_cast<int64_t>();
TVM_FFI_CHECK(int_opt.has_value(), ValueError) << "shape has unexpected type";
result.push_back(*int_opt);
sym_names.push_back("");
}
}
return SymShapeTuple{tvm::ffi::Shape(std::move(result)), sym_names};
}
} // namespace details
template <typename ValueType>
inline ValueType Lookup(const Object& json, const std::string& key) {
TVM_FFI_CHECK(json.count(key) != 0, ValueError)
<< "key `" << key << "` not found in the JSON object";
auto opt = json.at(key).try_cast<ValueType>();
TVM_FFI_CHECK(opt.has_value(), ValueError) << "key `" << key << "` has unexpected type";
return *opt;
}
template <typename ValueType>
inline ValueType Lookup(const Array& json, int index) {
TVM_FFI_ICHECK(index < static_cast<int>(json.size()))
<< "IndexError: json::array index out of range";
auto opt = json[index].try_cast<ValueType>();
TVM_FFI_ICHECK(opt.has_value()) << "ValueError: value at index `" << index
<< "` has unexpected type";
return *opt;
}
template <>
inline DLDataType Lookup(const Object& json, const std::string& key) {
return details::DTypeFromString(Lookup<std::string>(json, key));
}
template <>
inline DLDataType Lookup(const Array& json, int index) {
return details::DTypeFromString(Lookup<std::string>(json, index));
}
template <>
inline SymShapeTuple Lookup(const Object& json, const std::string& key) {
return details::SymShapeTupleFromArray(Lookup<Array>(json, key));
}
template <>
inline SymShapeTuple LookupOrDefault(const Object& json, const std::string& key,
const SymShapeTuple& default_value) {
if (json.count(key) == 0 || json.at(key) == nullptr) {
return default_value;
}
return details::SymShapeTupleFromArray(Lookup<Array>(json, key));
}
template <>
inline SymShapeTuple Lookup(const Array& json, int index) {
return details::SymShapeTupleFromArray(Lookup<Array>(json, index));
}
} // namespace json
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_JSON_PARSER_H_
+32
View File
@@ -0,0 +1,32 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/load_bytes_from_file.h
* \brief Utility methods to load from files.
*/
#ifndef MLC_LLM_SUPPORT_LOAD_BYTES_FROM_FILE_H_
#define MLC_LLM_SUPPORT_LOAD_BYTES_FROM_FILE_H_
#include <tvm/runtime/logging.h>
#include <fstream>
#include <string>
namespace mlc {
namespace llm {
inline std::string LoadBytesFromFile(const std::string& path) {
std::ifstream fs(path, std::ios::in | std::ios::binary);
TVM_FFI_ICHECK(!fs.fail()) << "Cannot open " << path;
std::string data;
fs.seekg(0, std::ios::end);
size_t size = static_cast<size_t>(fs.tellg());
fs.seekg(0, std::ios::beg);
data.resize(size);
fs.read(data.data(), size);
return data;
}
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_LOAD_BYTES_FROM_FILE_H_
+107
View File
@@ -0,0 +1,107 @@
/*!
* \file support/module_vtable.h
* \brief Compatibility shim providing the TVM_MODULE_VTABLE_* macros that
* previously lived in <tvm/runtime/module.h>. After the TVM runtime
* refactor the macros were moved into TVM's private
* src/runtime/vm/module_utils.h, so we vendor the surface mlc-llm uses
* here to keep the existing call sites unchanged.
*/
#ifndef MLC_LLM_SUPPORT_MODULE_VTABLE_H_
#define MLC_LLM_SUPPORT_MODULE_VTABLE_H_
#include <tvm/ffi/cast.h>
#include <tvm/ffi/extra/module.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/object.h>
#include <utility>
namespace mlc {
namespace llm {
namespace module_vtable_detail {
template <typename T>
struct EntryHelper {};
template <typename T, typename R, typename... Args>
struct EntryHelper<R (T::*)(Args...) const> {
using MemFnType = R (T::*)(Args...) const;
TVM_FFI_INLINE static void Call(::tvm::ffi::Any* rv, T* self, MemFnType f,
::tvm::ffi::PackedArgs args) {
auto wrapped = [self, f](Args... args) -> R { return (self->*f)(std::forward<Args>(args)...); };
::tvm::ffi::details::unpack_call<R>(std::make_index_sequence<sizeof...(Args)>{}, nullptr,
wrapped, args.data(), args.size(), rv);
}
};
template <typename T, typename R, typename... Args>
struct EntryHelper<R (T::*)(Args...)> {
using MemFnType = R (T::*)(Args...);
TVM_FFI_INLINE static void Call(::tvm::ffi::Any* rv, T* self, MemFnType f,
::tvm::ffi::PackedArgs args) {
auto wrapped = [self, f](Args... args) -> R { return (self->*f)(std::forward<Args>(args)...); };
::tvm::ffi::details::unpack_call<R>(std::make_index_sequence<sizeof...(Args)>{}, nullptr,
wrapped, args.data(), args.size(), rv);
}
};
template <typename T, typename... Args>
struct EntryHelper<void (T::*)(Args...) const> {
using MemFnType = void (T::*)(Args...) const;
TVM_FFI_INLINE static void Call(::tvm::ffi::Any* rv, T* self, MemFnType f,
::tvm::ffi::PackedArgs args) {
auto wrapped = [self, f](Args... args) -> void { (self->*f)(std::forward<Args>(args)...); };
::tvm::ffi::details::unpack_call<void>(std::make_index_sequence<sizeof...(Args)>{}, nullptr,
wrapped, args.data(), args.size(), rv);
}
};
template <typename T, typename... Args>
struct EntryHelper<void (T::*)(Args...)> {
using MemFnType = void (T::*)(Args...);
TVM_FFI_INLINE static void Call(::tvm::ffi::Any* rv, T* self, MemFnType f,
::tvm::ffi::PackedArgs args) {
auto wrapped = [self, f](Args... args) -> void { (self->*f)(std::forward<Args>(args)...); };
::tvm::ffi::details::unpack_call<void>(std::make_index_sequence<sizeof...(Args)>{}, nullptr,
wrapped, args.data(), args.size(), rv);
}
};
} // namespace module_vtable_detail
} // namespace llm
} // namespace mlc
#define TVM_MODULE_VTABLE_BEGIN(TypeKey) \
const char* kind() const final { return TypeKey; } \
::tvm::ffi::Optional<::tvm::ffi::Function> GetFunction(const ::tvm::ffi::String& _name) \
override { \
using SelfPtr = std::remove_cv_t<decltype(this)>; \
::tvm::ffi::ObjectPtr<::tvm::ffi::Object> _self = \
::tvm::ffi::GetObjectPtr<::tvm::ffi::Object>(this);
#define TVM_MODULE_VTABLE_END() \
return std::nullopt; \
}
#define TVM_MODULE_VTABLE_END_WITH_DEFAULT(MemFunc) \
{ \
auto f = (MemFunc); \
return (this->*f)(_name); \
} \
}
#define TVM_MODULE_VTABLE_ENTRY(Name, MemFunc) \
if (_name == Name) { \
return ::tvm::ffi::Function::FromPacked( \
[_self](::tvm::ffi::PackedArgs args, ::tvm::ffi::Any* rv) -> void { \
using Helper = ::mlc::llm::module_vtable_detail::EntryHelper<decltype(MemFunc)>; \
SelfPtr self = static_cast<SelfPtr>(_self.get()); \
Helper::Call(rv, self, MemFunc, args); \
}); \
}
#define TVM_MODULE_VTABLE_ENTRY_PACKED(Name, MemFunc) \
if (_name == Name) { \
return ::tvm::ffi::Function( \
[_self](::tvm::ffi::PackedArgs args, ::tvm::ffi::Any* rv) -> void { \
(static_cast<SelfPtr>(_self.get())->*(MemFunc))(args, rv); \
}); \
}
#endif // MLC_LLM_SUPPORT_MODULE_VTABLE_H_
+49
View File
@@ -0,0 +1,49 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/progress_bar.h
* \brief A simple progress bar in C++.
*/
#ifndef MLC_LLM_SUPPORT_PROGRESS_BAR_H_
#define MLC_LLM_SUPPORT_PROGRESS_BAR_H_
#include <iostream>
#include <string>
namespace mlc {
namespace llm {
class ProgressBar {
public:
explicit ProgressBar(int total, int width = 100) : total(total), width(width), cur(0) {}
void Progress() {
if (cur < total) {
++cur;
}
int bar_width = width - 2; // Adjust for borders
int completed = static_cast<int>(static_cast<float>(cur) / total * bar_width);
int remaining = bar_width - completed;
std::cout << "[" //
<< std::string(completed, '=') //
<< ">" //
<< std::string(remaining, ' ') //
<< "] " //
<< " [" << cur << "/" << total << "]";
if (cur < total) {
std::cout << "\r";
std::cout.flush();
} else {
std::cout << std::endl; // Move to the next line after the progress bar is complete
}
}
private:
int total;
int width;
int cur;
};
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_PROGRESS_BAR_H_
+37
View File
@@ -0,0 +1,37 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/random.h
* \brief Header of random number generator.
*/
#ifndef MLC_LLM_SUPPORT_RANDOM_H_
#define MLC_LLM_SUPPORT_RANDOM_H_
#include <random>
namespace mlc {
namespace llm {
// Random number generator
class RandomGenerator {
private:
std::mt19937 gen;
std::uniform_real_distribution<> dis;
public:
RandomGenerator(int seed = std::random_device{}()) : gen(seed), dis(0.0, 1.0) {}
static RandomGenerator& GetInstance(int seed = std::random_device{}()) {
static RandomGenerator instance(seed);
return instance;
}
double GetRandomNumber() { return dis(gen); }
void SetSeed(int seed) { gen.seed(seed); }
};
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_RANDOM_H_
+77
View File
@@ -0,0 +1,77 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/result.h
* \brief The header for the Result class in MLC LLM.
*/
#ifndef MLC_LLM_SUPPORT_RESULT_H_
#define MLC_LLM_SUPPORT_RESULT_H_
#include <tvm/runtime/logging.h>
#include <optional>
#include <string>
namespace mlc {
namespace llm {
/*!
* \brief The result class in MLC LLM.
* Each instance is either an okay value or an error.
* \tparam T The okay value type of the result.
* \tparam E The error type of the result.
*/
template <typename T, typename E = std::string>
class Result {
public:
/*! \brief Create a result with an okay value. */
static Result Ok(T value) {
Result result;
result.ok_value_ = std::move(value);
return result;
}
/*! \brief Create a result with an error value. */
static Result Error(E error) {
Result result;
result.err_value_ = std::move(error);
return result;
}
/*! \brief Check if the result is okay or not. */
bool IsOk() const { return ok_value_.has_value(); }
/*! \brief Check if the result is an error or not. */
bool IsErr() const { return err_value_.has_value(); }
/*!
* \brief Unwrap the result and return the okay value.
* Throwing exception if it is an error.
* \note This function returns the ok value by moving, so a Result can be unwrapped only once.
*/
T Unwrap() {
TVM_FFI_ICHECK(ok_value_.has_value()) << "Cannot unwrap result on an error value.";
TVM_FFI_ICHECK(!unwrapped_) << "Cannot unwrap a Result instance twice.";
unwrapped_ = true;
return std::move(ok_value_.value());
}
/*!
* \brief Unwrap the result and return the error value.
* Throwing exception if it is an okay value.
* \note This function returns the error value by moving, so a Result can be unwrapped only once.
*/
E UnwrapErr() {
TVM_FFI_ICHECK(err_value_.has_value()) << "Cannot unwrap result on an okay value.";
TVM_FFI_ICHECK(!unwrapped_) << "Cannot unwrap a Result instance twice.";
unwrapped_ = true;
return std::move(err_value_.value());
}
private:
/*! \brief A boolean flag indicating if the result is okay or error. */
bool unwrapped_ = false;
/*! \brief The internal optional okay value. */
std::optional<T> ok_value_;
/*! \brief The internal optional error value. */
std::optional<E> err_value_;
};
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_RESULT_H_
+73
View File
@@ -0,0 +1,73 @@
/*!
* \file support/threading_backend.h
* \brief Compatibility shim providing the threading helpers that used to live
* at <tvm/runtime/threading_backend.h>. The public TVM header was
* removed in a runtime refactor, but the underlying symbols
* (TVMBackendParallelLaunch, threading::MaxConcurrency,
* threading::SetMaxConcurrency) are still exported from libtvm_runtime.
* We re-declare the inline parallel-for template here so existing
* mlc-llm call sites keep compiling.
*/
#ifndef MLC_LLM_SUPPORT_THREADING_BACKEND_H_
#define MLC_LLM_SUPPORT_THREADING_BACKEND_H_
#include <tvm/runtime/base.h>
#include <tvm/runtime/c_backend_api.h>
#include <algorithm>
#include <cstdint>
namespace tvm {
namespace runtime {
namespace threading {
/*! \return the maximum number of effective workers for this system. */
int MaxConcurrency();
/*! \brief Setting the maximum number of available cores. */
void SetMaxConcurrency(int value);
} // namespace threading
namespace detail {
template <typename T>
struct ParallelForWithThreadingBackendLambdaInvoker {
static int TVMParallelLambdaInvoke(int task_id, TVMParallelGroupEnv* penv, void* cdata) {
int num_task = penv->num_task;
T* lambda_ptr = static_cast<T*>(cdata);
(*lambda_ptr)(task_id, num_task);
return 0;
}
};
template <typename T>
inline void parallel_launch_with_threading_backend(T flambda) {
void* cdata = &flambda;
TVMBackendParallelLaunch(ParallelForWithThreadingBackendLambdaInvoker<T>::TVMParallelLambdaInvoke,
cdata, /*num_task=*/0);
}
} // namespace detail
template <typename T>
inline void parallel_for_with_threading_backend(T flambda, int64_t begin, int64_t end) {
if (end - begin == 1) {
flambda(begin);
return;
}
auto flaunch = [begin, end, flambda](int task_id, int num_task) {
int64_t total_len = end - begin;
int64_t step = (total_len + num_task - 1) / num_task;
int64_t local_begin = std::min(begin + step * task_id, end);
int64_t local_end = std::min(local_begin + step, end);
for (int64_t i = local_begin; i < local_end; ++i) {
flambda(i);
}
};
detail::parallel_launch_with_threading_backend(flaunch);
}
} // namespace runtime
} // namespace tvm
#endif // MLC_LLM_SUPPORT_THREADING_BACKEND_H_
+79
View File
@@ -0,0 +1,79 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/utils.h
* \brief Utility functions.
*/
#ifndef MLC_LLM_SUPPORT_UTILS_H_
#define MLC_LLM_SUPPORT_UTILS_H_
#include <tvm/support/io.h>
#include <sstream>
#include <string>
#include <vector>
#include "../../3rdparty/tvm/src/support/base64.h"
#include "../../3rdparty/tvm/src/support/bytes_io.h"
namespace mlc {
namespace llm {
/*! \brief Split the input string by the given delimiter character. */
inline std::vector<std::string> Split(const std::string& str, char delim) {
std::string item;
std::istringstream is(str);
std::vector<std::string> ret;
while (std::getline(is, item, delim)) {
ret.push_back(item);
}
return ret;
}
/*!
* \brief Check whether the string starts with a given prefix.
* \param str The given string.
* \param prefix The given prefix.
* \return Whether the prefix matched.
*/
inline bool StartsWith(const std::string& str, const char* prefix) {
size_t n = str.length();
for (size_t i = 0; i < n; i++) {
if (prefix[i] == '\0') return true;
if (str.data()[i] != prefix[i]) return false;
}
// return true if the str is equal to the prefix
return prefix[n] == '\0';
}
/*!
* \brief Get the base64 encoded result of a string.
* \param str The string to encode.
* \return The base64 encoded string.
*/
inline std::string Base64Encode(std::string str) {
std::string result;
tvm::support::BytesOutStream m_stream(&result);
tvm::support::Base64OutStream b64stream(&m_stream);
static_cast<tvm::support::Stream*>(&b64stream)->Write(str);
b64stream.Finish();
return result;
}
/*!
* \brief Get the base64 decoded result of a string.
* \param str The string to decode.
* \return The base64 decoded string.
*/
inline std::string Base64Decode(std::string str) {
std::string result;
tvm::support::BytesInStream m_stream(str);
tvm::support::Base64InStream b64stream(&m_stream);
b64stream.InitPosition();
static_cast<tvm::support::Stream*>(&b64stream)->Read(&result);
return result;
}
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_UTILS_H_
+59
View File
@@ -0,0 +1,59 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/image_utils.cc
*/
#include "vlm_utils.h"
#include <cmath>
namespace mlc {
namespace llm {
void CalculateResizeShape(tvm::runtime::Tensor image_data, std::string model_type,
int* p_target_height, int* p_target_width) {
TVM_FFI_ICHECK_EQ(image_data->shape[3], 3) << "Image format must be NHWC";
int height = image_data->shape[1];
int width = image_data->shape[2];
if ("phi3_v" == model_type) {
const int hd_num = 4;
double ratio = static_cast<double>(width) / height;
int scale = 1;
while (scale * std::ceil(scale / ratio) <= hd_num) {
scale += 1;
}
scale -= 1;
*p_target_width = static_cast<int>(scale * 336);
*p_target_height = static_cast<int>(*p_target_width / ratio);
}
}
void CalculatePadShape(tvm::runtime::Tensor image_data, std::string model_type, int* p_pad_height,
int* p_pad_width) {
TVM_FFI_ICHECK_EQ(image_data->shape[3], 3) << "Image format must be NHWC";
if ("phi3_v" == model_type) {
int resized_height = 0, resized_width = 0;
CalculateResizeShape(image_data, model_type, &resized_height, &resized_width);
int tar = (int)(ceil(resized_height / 336.0) * 336);
int top_padding = (int)((tar - resized_height) / 2);
int bottom_padding = tar - resized_height - top_padding;
TVM_FFI_ICHECK_EQ(tar, resized_height + top_padding + bottom_padding)
<< "Padding size not equal!";
*p_pad_height = tar;
*p_pad_width = resized_width;
}
}
void CalculateCropShape(tvm::runtime::Tensor image_data, std::string model_type, int* p_crop_height,
int* p_crop_width) {
TVM_FFI_ICHECK_EQ(image_data->shape[3], 3) << "Image format must be NHWC";
if ("phi3_v" == model_type) {
int pad_h = 0, pad_w = 0;
CalculatePadShape(image_data, model_type, &pad_h, &pad_w);
*p_crop_height = pad_h / 336;
*p_crop_width = pad_w / 336;
}
}
} // namespace llm
} // namespace mlc
+48
View File
@@ -0,0 +1,48 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file support/vlm_utils.h
* \brief Tools for debug purposes.
*/
#ifndef MLC_LLM_SUPPORT_VLM_UTILS_H_
#define MLC_LLM_SUPPORT_VLM_UTILS_H_
#include <tvm/runtime/tensor.h>
#include <string>
namespace mlc {
namespace llm {
/*!
* \brief Calculate the target height and width for resizing an image based on the input data and
* model type. \param image_data The input image data as a TVM Tensor. \param model_type The type
* of the model influencing the resizing parameters (e.g., phi3v). \param target_height Reference to
* the variable where the calculated target height will be stored. \param target_width Reference to
* the variable where the calculated target width will be stored.
*/
void CalculateResizeShape(tvm::runtime::Tensor image_data, std::string model_type,
int* p_target_height, int* p_target_width);
/*!
* \brief Calculate the padding height and width for an image based on the input data and model
* type. \param image_data The input image data as a TVM Tensor. \param model_type The type of the
* model influencing the padding parameters (e.g., phi3v). \param pad_height Reference to the
* variable where the calculated padding height will be stored. \param pad_width Reference to the
* variable where the calculated padding width will be stored.
*/
void CalculatePadShape(tvm::runtime::Tensor image_data, std::string model_type, int* p_pad_height,
int* p_pad_width);
/*!
* \brief Calculate the cropping height and width for an image based on the input data and model
* type. \param image_data The input image data as a TVM Tensor. \param model_type The type of the
* model influencing the cropping parameters (e.g., phi3v). \param crop_height Reference to the
* variable where the calculated cropping height will be stored. \param crop_width Reference to the
* variable where the calculated cropping width will be stored.
*/
void CalculateCropShape(tvm::runtime::Tensor image_data, std::string model_type, int* p_crop_height,
int* p_crop_width);
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_SUPPORT_IMAGE_UTILS_H_
+292
View File
@@ -0,0 +1,292 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file streamer.cc
*/
#include "streamer.h"
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include <algorithm>
#include <string>
#include "tokenizers.h"
namespace mlc {
namespace llm {
TVM_FFI_STATIC_INIT_BLOCK() {
TextStreamerObj::RegisterReflection();
StopStrHandlerObj::RegisterReflection();
}
/****************** TextStreamer ******************/
TextStreamerObj::TextStreamerObj(Tokenizer tokenizer) : tokenizer_(std::move(tokenizer)) {}
TextStreamer::TextStreamer(Tokenizer tokenizer) {
data_ = tvm::ffi::make_object<TextStreamerObj>(std::move(tokenizer));
}
std::string TextStreamerObj::Put(const std::vector<int32_t>& delta_tokens) {
TVM_FFI_ICHECK(!finished_) << "`put` is not expected to be invoked after finish.";
if (delta_tokens.empty()) {
return "";
}
std::string ret;
// We process delta tokens one by one.
for (int32_t delta_token : delta_tokens) {
// push to pending tokens.
pending_tokens_.push_back(delta_token);
// all_tokens = prefix_tokens_ + pending_tokens_
std::vector<int32_t> all_tokens;
all_tokens.reserve(prefix_tokens_.size() + pending_tokens_.size());
all_tokens.insert(all_tokens.end(), prefix_tokens_.begin(), prefix_tokens_.end());
all_tokens.insert(all_tokens.end(), pending_tokens_.begin(), pending_tokens_.end());
// Decode prefix_tokens_ and all_tokens.
std::string prefix_str = prefix_tokens_.empty() ? "" : tokenizer_->Decode(prefix_tokens_);
std::string full_str = tokenizer_->Decode(all_tokens);
std::string validated_str;
std::vector<int32_t> new_pending_tokens;
if (full_str.compare(0, prefix_str.length(), prefix_str) == 0) {
// Case 1. prefix_str is a prefix of `full_str`.
// validated_str = full_str[len(prefix_str):]
validated_str = full_str.substr(prefix_str.length());
// Pop UTF-8 replacement character from the back of pending tokens.
// - The UTF-8 replacement character take 3 chars.
// - A valid UTF-8 has 4 chars at most.
// So there will be at most 3 tokens popped.
while (!pending_tokens_.empty() && //
static_cast<int>(new_pending_tokens.size()) < 3 && //
validated_str.length() >= 3 && //
validated_str.compare(validated_str.length() - 3, /*n=*/3, kReplacementCharacter) ==
0) {
new_pending_tokens.push_back(pending_tokens_.back());
pending_tokens_.pop_back();
all_tokens.pop_back();
validated_str = tokenizer_->Decode(all_tokens).substr(prefix_str.length());
}
} else {
// Case 2. prefix_str is not a prefix of `full_str`.
// Pop pending tokens from the back.
// - Pop until prefix_str is indeed a prefix of full_str.
// - A valid UTF-8 has 4 chars at most.
// So there will be at most 3 tokens popped.
// - If there are no more than 3 pending tokens, skip popping.
// This is because it is impossible to make full_str contain
// prefix_str without popping all the pending tokens.
if (static_cast<int>(pending_tokens_.size()) < 3) {
continue;
}
bool get_valid_full_str = false;
while (!pending_tokens_.empty() && static_cast<int>(new_pending_tokens.size()) < 3) {
new_pending_tokens.push_back(pending_tokens_.back());
pending_tokens_.pop_back();
all_tokens.pop_back();
full_str = tokenizer_->Decode(all_tokens);
if (full_str.compare(0, prefix_str.length(), prefix_str) == 0) {
get_valid_full_str = true;
break;
}
}
if (get_valid_full_str) {
// We find a full_str which starts from prefix_str.
// So we return the sliced full string without the prefix.
validated_str = full_str.substr(prefix_str.length());
} else {
// We cannot find a full_str which starts from prefix_str by
// popping 3 tokens.
// In this case, the remaining pending tokens are invalid UTF-8
// characters already, so we return the decoded pending tokens.
validated_str = tokenizer_->Decode(pending_tokens_);
}
}
if (!pending_tokens_.empty()) {
// Set the new prefix.
prefix_tokens_ = pending_tokens_;
}
std::reverse(new_pending_tokens.begin(), new_pending_tokens.end());
pending_tokens_ = new_pending_tokens;
ret += validated_str;
}
return ret;
}
std::string TextStreamerObj::Finish() {
// all_tokens = prefix_tokens_ + pending_tokens_
std::vector<int32_t> all_tokens;
all_tokens.reserve(prefix_tokens_.size() + pending_tokens_.size());
all_tokens.insert(all_tokens.end(), prefix_tokens_.begin(), prefix_tokens_.end());
all_tokens.insert(all_tokens.end(), pending_tokens_.begin(), pending_tokens_.end());
// Decode prefix_tokens_ and all_tokens.
std::string prefix_str = prefix_tokens_.empty() ? "" : tokenizer_->Decode(prefix_tokens_);
std::string full_str = all_tokens.empty() ? "" : tokenizer_->Decode(all_tokens);
finished_ = true;
if (full_str.compare(0, prefix_str.length(), prefix_str) == 0) {
// Case 1. prefix_str is a prefix of `full_str`.
return full_str.substr(prefix_str.length());
} else {
// Case 2. prefix_str is not a prefix of `full_str`.
// In this case, the remaining pending tokens are invalid UTF-8
// characters already, so we return the decoded pending tokens.
return tokenizer_->Decode(pending_tokens_);
}
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("mlc.tokenizers.TextStreamer",
[](Tokenizer tokenizer) { return TextStreamer(std::move(tokenizer)); })
.def("mlc.tokenizers.TextStreamerPut",
[](TextStreamer text_streamer, const Shape& delta_tokens) {
return text_streamer->Put(
{delta_tokens->data, delta_tokens->data + delta_tokens->size});
})
.def_method("mlc.tokenizers.TextStreamerFinish", &TextStreamerObj::Finish);
}
/****************** StopStrHandler ******************/
/*! \brief Create the KMP partial match table for the input string. */
inline std::vector<int> CreatePartialMatchTable(const String& str) {
int length = str.length();
std::vector<int> partial_match_table = {-1};
partial_match_table.reserve(length);
for (int i = 1; i < length; ++i) {
int ptr = partial_match_table[i - 1];
while (ptr != -1 && str.at(ptr) != str.at(i - 1)) {
ptr = partial_match_table[ptr];
}
partial_match_table.push_back(ptr + 1);
}
return partial_match_table;
}
StopStrHandlerObj::StopStrHandlerObj(Array<String> stop_strs,
const std::vector<std::string>& token_table)
: stop_strs_(std::move(stop_strs)), token_table_(token_table) {
int num_stop_strs = stop_strs_.size();
cur_match_lengths_.resize(num_stop_strs, 0);
// Create the KMP partial match table for each stop string.
partial_match_tables_.reserve(num_stop_strs);
for (const String& stop_str : stop_strs_) {
TVM_FFI_ICHECK(!stop_str.empty()) << "Stop string cannot be empty.";
partial_match_tables_.push_back(CreatePartialMatchTable(stop_str));
}
}
void StopStrHandlerObj::Put(int32_t token_id, std::vector<int64_t>* return_token_ids) {
TVM_FFI_ICHECK_NOTNULL(return_token_ids);
// Return the input token id if there is no stop string.
if (stop_strs_.empty()) {
return_token_ids->push_back(token_id);
return;
}
TVM_FFI_ICHECK(!stop_triggered_) << "Cannot put new token when already stopped.";
TVM_FFI_ICHECK_LT(token_id, static_cast<int>(token_table_.size()));
const std::string& token = token_table_[token_id];
pending_token_ids_.push_back(token_id);
pending_token_lengths_.push_back(token.length());
for (char ch : token) {
// The earliest starting point of stop string.
int stop_starting_pos = std::numeric_limits<int>::max();
// The cutoff length that can be safely return.
int cutoff_length = std::numeric_limits<int>::max();
// The maximum matched length.
int max_match_length = 0;
for (int str_id = 0; str_id < static_cast<int>(stop_strs_.size()); ++str_id) {
// - Run one step of KMP algorithm.
const std::vector<int>& partial_match_table = partial_match_tables_[str_id];
int& cur_match_length = cur_match_lengths_[str_id];
while (cur_match_length != -1 && ch != stop_strs_[str_id].at(cur_match_length)) {
cur_match_length = partial_match_table[cur_match_length];
}
++cur_match_length;
// Case 1. The stop string is matched.
if (cur_match_length == stop_strs_[str_id].length()) {
stop_triggered_ = true;
stop_starting_pos =
std::min(stop_starting_pos,
pending_string_len_ + 1 - static_cast<int>(stop_strs_[str_id].length()));
continue;
}
// Case 2. The stop string is not matched.
// - Get the cutoff length that can be safely return.
TVM_FFI_ICHECK_GE(pending_string_len_ + 1, cur_match_length);
cutoff_length = std::min(cutoff_length, pending_string_len_ + 1 - cur_match_length);
// - Get the updated pending string length.
max_match_length = std::max(max_match_length, cur_match_length);
}
// Collect the token ids that can be safely cut off and returned.
if (stop_triggered_) {
cutoff_length = stop_starting_pos;
}
TVM_FFI_ICHECK_NE(cutoff_length, std::numeric_limits<int>::max());
TVM_FFI_ICHECK_GE(cutoff_length, 0);
int cum_length = 0;
while (!pending_token_ids_.empty() &&
cum_length + pending_token_lengths_.front() <= cutoff_length) {
cum_length += pending_token_lengths_.front();
return_token_ids->push_back(pending_token_ids_.front());
pending_token_ids_.erase(pending_token_ids_.begin());
pending_token_lengths_.erase(pending_token_lengths_.begin());
}
if (stop_triggered_) {
return;
}
TVM_FFI_ICHECK_LE(cum_length, cutoff_length);
// `cum_length` is the prefix length what we actually cut off.
pending_string_len_ = (cutoff_length - cum_length) + max_match_length;
}
}
StopStrHandler::StopStrHandler(Array<String> stop_strs,
const std::vector<std::string>& token_table) {
data_ = tvm::ffi::make_object<StopStrHandlerObj>(std::move(stop_strs), token_table);
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("mlc.tokenizers.StopStrHandler",
[](Array<String> stop_strs, const Tokenizer& tokenizer) {
return StopStrHandler(std::move(stop_strs), tokenizer->PostProcessedTokenTable());
})
.def("mlc.tokenizers.StopStrHandlerPut",
[](StopStrHandler handler, int token_id) {
std::vector<int64_t> delta_tokens;
handler->Put(token_id, &delta_tokens);
return Shape(std::move(delta_tokens));
})
.def("mlc.tokenizers.StopStringHandlerFinish",
[](StopStrHandler handler) {
std::vector<int64_t> remaining_token_ids;
handler->Finish(&remaining_token_ids);
return Shape(std::move(remaining_token_ids));
})
.def_method("mlc.tokenizers.StopStrHandlerStopTriggered", &StopStrHandlerObj::StopTriggered);
}
} // namespace llm
} // namespace mlc
+160
View File
@@ -0,0 +1,160 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file streamer.h
* \brief Header of streamers in MLC LLM.
*/
#ifndef MLC_LLM_STREAMER_H_
#define MLC_LLM_STREAMER_H_
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
#include "tokenizers.h"
namespace mlc {
namespace llm {
using namespace tvm::runtime;
using tvm::ffi::Object;
using tvm::ffi::ObjectRef;
/****************** TextStreamer ******************/
/*!
* \brief The class that streams back validated utf-8 text strings
* that generated by tokenizer.
*/
class TextStreamerObj : public Object {
public:
explicit TextStreamerObj(Tokenizer tokenizer);
/*!
* \brief Put new delta tokens into the streamer, and get the UTF-8-valid
* delta string. The text streamer may hold some of the input delta tokens
* which cannot decode into valid UTF-8 strings. The returned string
* is always guaranteed to be UTF-8 valid.
* \param delta_tokens The new tokens to put into the streamer.
* \return The decoded delta string after putting the input new tokens.
*/
std::string Put(const std::vector<int32_t>& delta_tokens);
/*! \brief Return the string decoded by remaining tokens. */
std::string Finish();
// REPLACEMENT CHARACTER (U+FFFD) in UTF-8.
static constexpr const char* kReplacementCharacter = "\xef\xbf\xbd";
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<TextStreamerObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.TextStreamer", TextStreamerObj, Object);
private:
Tokenizer tokenizer_;
std::vector<int32_t> prefix_tokens_;
std::vector<int32_t> pending_tokens_;
bool finished_ = false;
};
/*!
* \brief Managed reference to TextStreamerObj
* \sa TextStreamerObj
*/
class TextStreamer : public ObjectRef {
public:
/*! \brief Construct a text streamer with tokenizer. */
explicit TextStreamer(Tokenizer tokenizer);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TextStreamer, ObjectRef, TextStreamerObj);
};
/****************** StopStrHandler ******************/
/*!
* \brief The stop string handler in MLC LLM, which takes input delta tokens
* one at a time, and return the output delta token before stopping due to
* stop strings.
*/
class StopStrHandlerObj : public Object {
public:
explicit StopStrHandlerObj(Array<String> stop_strs, const std::vector<std::string>& token_table);
/*!
* \brief Add new input delta token to the handler, push the output
* delta tokens before stopping into the given vector.
* The stop string handler may hold some of the input delta token
* which may be part of a stop string.
* The returned tokens are always guaranteed not to be part of stop string.
*/
void Put(int32_t token_id, std::vector<int64_t>* return_token_ids);
/*!
* \brief Stop string handling has finished, append the remaining
* cached token ids into the given vector.
*/
void Finish(std::vector<int64_t>* return_token_ids) const {
return_token_ids->insert(return_token_ids->end(), pending_token_ids_.begin(),
pending_token_ids_.end());
};
/*! \brief Check if the generation has stopped due to stop string. */
bool StopTriggered() const { return stop_triggered_; }
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<StopStrHandlerObj>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("mlc.StopStrHandler", StopStrHandlerObj, Object);
private:
/*! \brief The stop strings. */
Array<String> stop_strs_;
/*! \brief The partial match table for each stop string in the KMP algorithm. */
std::vector<std::vector<int>> partial_match_tables_;
/*! \brief The tokenizer token table for token id lookup. */
const std::vector<std::string>& token_table_;
/************ Global states across all stop strings. ************/
/*! \brief The globally pending string length. */
int pending_string_len_ = 0;
/*! \brief The globally pending token ids. */
std::vector<int32_t> pending_token_ids_;
/*! \brief The token string length of each pending token id. */
std::vector<int> pending_token_lengths_;
/*! \brief A boolean flag indicating if stop has been triggered. */
bool stop_triggered_ = false;
/************ Per-stop-string states. ************/
/*! \brief The current match position of the pending string to each stop string. */
std::vector<int> cur_match_lengths_;
};
/*!
* \brief Managed reference to StopStrHandlerObj
* \sa StopStrHandlerObj
*/
class StopStrHandler : public ObjectRef {
public:
explicit StopStrHandler(Array<String> stop_strs, const std::vector<std::string>& token_table);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(StopStrHandler, ObjectRef, StopStrHandlerObj);
};
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_STREAMER_H_
+535
View File
@@ -0,0 +1,535 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file tokenizer.cc
*/
#include "tokenizers.h"
#include <tokenizers_cpp.h>
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/runtime/logging.h>
#include <array>
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
#include "./../support/encoding.h"
#include "./../support/load_bytes_from_file.h"
namespace mlc {
namespace llm {
TVM_FFI_STATIC_INIT_BLOCK() {
TokenizerInfoNode::RegisterReflection();
TokenizerObj::RegisterReflection();
}
#ifndef COMPILE_MLC_WASM_RUNTIME
String TokenizerInfoNode::AsJSONString() const {
tvm::ffi::json::Object obj;
obj.Set("token_postproc_method", token_postproc_method);
obj.Set("prepend_space_in_encode", prepend_space_in_encode);
obj.Set("strip_space_in_decode", strip_space_in_decode);
return tvm::ffi::json::Stringify(obj);
}
TokenizerInfo TokenizerInfo::FromJSONString(String json_string) {
tvm::ffi::String err;
auto v = tvm::ffi::json::Parse(json_string, &err);
TVM_FFI_ICHECK(err.empty()) << "Failed to parse JSON: " << err;
TVM_FFI_ICHECK(v.try_cast<tvm::ffi::json::Object>().has_value()) << "JSON must be an object.";
const auto& obj = v.cast<tvm::ffi::json::Object>();
ObjectPtr<TokenizerInfoNode> n = tvm::ffi::make_object<TokenizerInfoNode>();
if (obj.count("token_postproc_method")) {
TVM_FFI_ICHECK(obj.at("token_postproc_method").try_cast<tvm::ffi::String>().has_value());
n->token_postproc_method = obj.at("token_postproc_method").cast<tvm::ffi::String>();
}
if (obj.count("prepend_space_in_encode")) {
TVM_FFI_ICHECK(obj.at("prepend_space_in_encode").try_cast<bool>().has_value());
n->prepend_space_in_encode = obj.at("prepend_space_in_encode").cast<bool>();
}
if (obj.count("strip_space_in_decode")) {
TVM_FFI_ICHECK(obj.at("strip_space_in_decode").try_cast<bool>().has_value());
n->strip_space_in_decode = obj.at("strip_space_in_decode").cast<bool>();
}
return TokenizerInfo(n);
}
Tokenizer::Tokenizer(std::unique_ptr<tokenizers::Tokenizer> tokenizer, TokenizerInfo info) {
ObjectPtr<TokenizerObj> n = tvm::ffi::make_object<TokenizerObj>();
n->tokenizer = std::move(tokenizer);
n->info_ = std::move(info);
data_ = std::move(n);
}
std::vector<int32_t> TokenizerObj::Encode(const std::string& text) const {
return tokenizer->Encode(text);
}
std::vector<int32_t> TokenizerObj::EncodeNoPrependSpace(const std::string& text) const {
// TODO(yixin): now this only supports tokenizers with tokenizer.json
// other tokenizers should be supported.
static const constexpr char* kPaddingPrefix = "\x01";
if (!info_->prepend_space_in_encode) {
return tokenizer->Encode(text);
}
auto result = tokenizer->Encode(kPaddingPrefix + text);
// remove the first two tokens: "▁" and "<0x01>"
result.erase(result.begin(), result.begin() + 2);
return result;
}
std::vector<std::vector<int32_t>> TokenizerObj::EncodeBatch(const Array<String>& texts) const {
std::vector<std::string> texts_vec;
for (const String& text : texts) {
texts_vec.push_back(text);
}
return tokenizer->EncodeBatch(texts_vec);
}
std::string TokenizerObj::Decode(const std::vector<int32_t>& token_ids) const {
return tokenizer->Decode(token_ids);
}
const DynamicBitset& TokenizerObj::GetPrefixTokenMask() {
if (prefix_token_mask_.Size() != 0) {
return prefix_token_mask_;
}
int vocab_size = GetVocabSize();
prefix_token_mask_ = DynamicBitset(vocab_size);
// Sort all tokens
const auto& token_table = PostProcessedTokenTable();
std::vector<std::pair<std::string, int>> sorted_tokens;
for (int32_t token_id = 0; token_id < vocab_size; ++token_id) {
sorted_tokens.emplace_back(token_table[token_id], token_id);
}
std::sort(sorted_tokens.begin(), sorted_tokens.end());
// Check every token if it is a prefix of another token
for (int idx = 0; idx < vocab_size - 1; ++idx) {
auto cur_token = sorted_tokens[idx].first;
auto nxt_token = sorted_tokens[idx + 1].first;
if (cur_token.length() <= nxt_token.length() &&
std::string_view(nxt_token).substr(0, cur_token.length()) == cur_token) {
prefix_token_mask_.Set(sorted_tokens[idx].second);
}
}
return prefix_token_mask_;
}
size_t TokenizerObj::GetVocabSize() const { return tokenizer->GetVocabSize(); }
std::string TokenizerObj::IdToToken(int32_t token_id) const {
return tokenizer->IdToToken(token_id);
}
int32_t TokenizerObj::TokenToId(const std::string& token) const {
return tokenizer->TokenToId(token);
}
Tokenizer Tokenizer::FromPath(const String& _path, std::optional<TokenizerInfo> info) {
TokenizerInfo info_value = info.value_or(DetectTokenizerInfo(_path));
std::filesystem::path path{std::string(_path)};
std::filesystem::path sentencepiece;
std::filesystem::path huggingface;
std::filesystem::path rwkvworld;
TVM_FFI_ICHECK(std::filesystem::exists(path)) << "Cannot find tokenizer via path: " << _path;
if (std::filesystem::is_directory(path)) {
sentencepiece = path / "tokenizer.model";
huggingface = path / "tokenizer.json";
rwkvworld = path / "tokenizer_model";
} else {
sentencepiece = path.parent_path() / "tokenizer.model";
huggingface = path.parent_path() / "tokenizer.json";
rwkvworld = path.parent_path() / "tokenizer_model";
}
if (std::filesystem::exists(huggingface)) {
// Check HuggingFace
return Tokenizer(tokenizers::Tokenizer::FromBlobJSON(LoadBytesFromFile(huggingface.string())),
info_value);
}
if (std::filesystem::exists(sentencepiece)) {
// Check SentencePiece
LOG(WARNING)
<< "Using `tokenizer.model` since we cannot locate `tokenizer.json`.\n"
<< "It is recommended to use `tokenizer.json` to ensure all token mappings are included, "
<< "since currently, files like `added_tokens.json`, `tokenizer_config.json` are ignored.\n"
<< "Consider converting `tokenizer.model` to `tokenizer.json` by compiling the model "
<< "with MLC again, or see if MLC's huggingface provides this file.";
return Tokenizer(
tokenizers::Tokenizer::FromBlobSentencePiece(LoadBytesFromFile(sentencepiece.string())),
info_value);
}
{
// Check ByteLevelBPE
std::filesystem::path merges_path = path / "merges.txt";
std::filesystem::path vocab_path = path / "vocab.json";
std::filesystem::path added_tokens_path = path / "added_tokens.json";
if (std::filesystem::exists(merges_path) && std::filesystem::exists(vocab_path) &&
std::filesystem::exists(added_tokens_path)) {
std::string vocab = LoadBytesFromFile(vocab_path.string());
std::string merges = LoadBytesFromFile(merges_path.string());
std::string added_tokens = LoadBytesFromFile(added_tokens_path.string());
return Tokenizer(tokenizers::Tokenizer::FromBlobByteLevelBPE(vocab, merges, added_tokens),
info_value);
}
}
if (std::filesystem::exists(rwkvworld)) {
// Check RWKV
return Tokenizer(tokenizers::Tokenizer::FromBlobRWKVWorld(rwkvworld.string()), info_value);
}
LOG(FATAL) << "Cannot find any tokenizer under: " << _path;
}
TokenizerInfo Tokenizer::DetectTokenizerInfo(const String& path_str) {
std::filesystem::path path{std::string(path_str)};
TVM_FFI_ICHECK(std::filesystem::exists(path)) << "Cannot find tokenizer via path: " << path_str;
if (!std::filesystem::is_directory(path)) {
path = path.parent_path();
}
path = path / "tokenizer.json";
if (!std::filesystem::exists(path)) {
LOG(WARNING) << "Tokenizer info is not detected as tokenizer.json is not found. The default "
<< "tokenizer info will be used.";
return TokenizerInfo(tvm::ffi::make_object<TokenizerInfoNode>());
}
std::string tokenizer_json = LoadBytesFromFile(path.string());
tvm::ffi::String err;
auto v = tvm::ffi::json::Parse(tokenizer_json, &err);
TVM_FFI_ICHECK(err.empty()) << "Failed to parse JSON: " << err;
TVM_FFI_ICHECK(v.try_cast<tvm::ffi::json::Object>().has_value()) << "JSON must be an object.";
const auto& obj = v.cast<tvm::ffi::json::Object>();
ObjectPtr<TokenizerInfoNode> n = tvm::ffi::make_object<TokenizerInfoNode>();
// Step 1. Detect token_postproc_method: byte_fallback or byte_level
// Detect {"type": "ByteLevel"} or {"type": "ByteFallback"} in "decoder" field of the tokenizer
if (!obj.count("decoder") || !obj.at("decoder").try_cast<tvm::ffi::json::Object>().has_value()) {
LOG(WARNING) << "Decoder field is not found in tokenizer.json. Use ByteFallback as default.";
n->token_postproc_method = "byte_fallback";
} else {
auto decoder_obj = obj.at("decoder").cast<tvm::ffi::json::Object>();
TVM_FFI_ICHECK(decoder_obj.count("type") &&
decoder_obj.at("type").try_cast<tvm::ffi::String>().has_value());
auto type = decoder_obj.at("type").cast<tvm::ffi::String>();
auto f_detect_decoder_type = [](ObjectPtr<TokenizerInfoNode> n,
const tvm::ffi::json::Value& decoder_json) {
TVM_FFI_ICHECK(decoder_json.try_cast<tvm::ffi::json::Object>().has_value());
TVM_FFI_ICHECK(decoder_json.cast<tvm::ffi::json::Object>().count("type") &&
decoder_json.cast<tvm::ffi::json::Object>()
.at("type")
.try_cast<tvm::ffi::String>()
.has_value());
auto type = decoder_json.cast<tvm::ffi::json::Object>().at("type").cast<tvm::ffi::String>();
if (type == "ByteLevel") {
n->token_postproc_method = "byte_level";
return true;
} else if (type == "ByteFallback") {
n->token_postproc_method = "byte_fallback";
return true;
}
return false;
};
bool found = false;
// For sequence, examine every decoder
if (type == "Sequence") {
TVM_FFI_ICHECK(decoder_obj.count("decoders") &&
decoder_obj.at("decoders").try_cast<tvm::ffi::json::Array>().has_value());
for (const tvm::ffi::json::Value& decoder :
decoder_obj.at("decoders").cast<tvm::ffi::json::Array>()) {
if (f_detect_decoder_type(n, decoder)) {
found = true;
}
}
} else {
if (f_detect_decoder_type(n, obj.at("decoder"))) {
found = true;
}
}
if (!found) {
LOG(WARNING) << "Neither ByteLevel nor ByteFallback decoder is detected in tokenizer.json. "
<< "Use ByteFallback as default.";
n->token_postproc_method = "byte_fallback";
}
}
// Step 2. Detect prepend_space_in_encode
// Find {"type": "Prepend", "prepend": "▁"} in "normalizer" field of the tokenizer
if (obj.count("normalizer") &&
obj.at("normalizer").try_cast<tvm::ffi::json::Object>().has_value()) {
const tvm::ffi::json::Value& normalizer_json = obj.at("normalizer");
auto f_handle_normalizer = [](ObjectPtr<TokenizerInfoNode> n,
const tvm::ffi::json::Value& normalizer_json) {
TVM_FFI_ICHECK(normalizer_json.try_cast<tvm::ffi::json::Object>().has_value());
auto obj = normalizer_json.cast<tvm::ffi::json::Object>();
TVM_FFI_ICHECK(obj.count("type") && obj.at("type").try_cast<tvm::ffi::String>().has_value());
if (obj.at("type").cast<tvm::ffi::String>() == "Prepend" && obj.count("prepend") &&
obj.at("prepend").try_cast<tvm::ffi::String>().has_value() &&
obj.at("prepend").cast<tvm::ffi::String>() == "\xe2\x96\x81") {
n->prepend_space_in_encode = true;
return true;
}
return false;
};
auto type = normalizer_json.cast<tvm::ffi::json::Object>().at("type").cast<tvm::ffi::String>();
if (type == "Sequence") {
TVM_FFI_ICHECK(normalizer_json.cast<tvm::ffi::json::Object>().count("normalizers") &&
normalizer_json.cast<tvm::ffi::json::Object>()
.at("normalizers")
.try_cast<tvm::ffi::json::Array>()
.has_value());
for (const tvm::ffi::json::Value& normalizer : normalizer_json.cast<tvm::ffi::json::Object>()
.at("normalizers")
.cast<tvm::ffi::json::Array>()) {
if (f_handle_normalizer(n, normalizer)) {
break;
}
}
} else {
f_handle_normalizer(n, normalizer_json);
}
}
// Step 3. Detect strip_space_in_decode
// Find {"type": "Strip", "content": " ", "start": 1, "stop": 0} in "decoder" field of the
// tokenizer
if (obj.count("decoder") && obj.at("decoder").try_cast<tvm::ffi::json::Object>().has_value()) {
const tvm::ffi::json::Value& decoders_json = obj.at("decoder");
auto f_handle_decoder = [](ObjectPtr<TokenizerInfoNode> n,
const tvm::ffi::json::Value& decoder_json) {
TVM_FFI_ICHECK(decoder_json.try_cast<tvm::ffi::json::Object>().has_value());
auto obj = decoder_json.cast<tvm::ffi::json::Object>();
TVM_FFI_ICHECK(obj.count("type") && obj.at("type").try_cast<tvm::ffi::String>().has_value());
if (obj.at("type").cast<tvm::ffi::String>() == "Strip" && obj.count("content") &&
obj.at("content").try_cast<tvm::ffi::String>().has_value() &&
obj.at("content").cast<tvm::ffi::String>() == " " && obj.count("start") &&
obj.at("start").try_cast<int64_t>().has_value() && obj.at("start").cast<int64_t>() == 1 &&
obj.count("stop") && obj.at("stop").try_cast<int64_t>().has_value() &&
obj.at("stop").cast<int64_t>() == 0) {
n->strip_space_in_decode = true;
return true;
}
return false;
};
auto type = decoders_json.cast<tvm::ffi::json::Object>().at("type").cast<tvm::ffi::String>();
if (type == "Sequence") {
TVM_FFI_ICHECK(decoders_json.cast<tvm::ffi::json::Object>().count("decoders") &&
decoders_json.cast<tvm::ffi::json::Object>()
.at("decoders")
.try_cast<tvm::ffi::json::Array>()
.has_value());
for (const tvm::ffi::json::Value& decoder : decoders_json.cast<tvm::ffi::json::Object>()
.at("decoders")
.cast<tvm::ffi::json::Array>()) {
if (f_handle_decoder(n, decoder)) {
break;
}
}
} else {
f_handle_decoder(n, decoders_json);
}
}
return TokenizerInfo(n);
}
#endif
/*! \brief ByteFallback decoder: transform tokens like <0x1B> to hex char byte 1B */
inline std::string ByteFallbackDecoder(const std::string& token) {
if (token.length() == 6 && token.substr(0, 3) == "<0x" && token.back() == '>') {
int byte = 0;
for (int i = 0; i < 2; ++i) {
byte *= 16;
byte +=
token[3 + i] >= '0' && token[3 + i] <= '9' ? token[3 + i] - '0' : token[3 + i] - 'A' + 10;
}
TVM_FFI_ICHECK(byte >= 0 && byte < 256);
return std::string(/*n=*/1, static_cast<char>(byte));
}
return token;
}
/*! \brief SpaceReplacer decoder: transform "\u2581" back to space */
inline std::string SpaceReplacerDecoder(const std::string& token) {
// \u2581 is the unicode for "lower one eighth block"
// UTF8 encoding for \u2581 is 0xE2 0x96 0x81
std::string result;
for (size_t i = 0; i < token.size(); ++i) {
if (i + 2 < token.size() && token[i] == char(0xE2) && token[i + 1] == char(0x96) &&
token[i + 2] == char(0x81)) {
result += ' ';
i += 2;
} else {
result += token[i];
}
}
return result;
}
/*! \brief ByteLevel decoder: inverses the bytes-to-unicode transformation in the encoding
* process as in
* https://github.com/huggingface/transformers/blob/87be06ca77166e6a6215eee5a990ab9f07238a18/src/transformers/models/gpt2/tokenization_gpt2.py#L38-L59
*/
inline std::string ByteLevelDecoder(const std::string& token) {
// clang-format off
// The inverse map of bytes_to_unicode. -1 means there is no mapping to this unicode.
static const std::array<int, 324> char_to_byte_map = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68,
69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, -1,
174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191,
192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227,
228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245,
246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 127, 128,
129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146,
147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 173
};
// clang-format on
auto unicode_codepoints = ParseUTF8(token.c_str(), UTF8ErrorPolicy::kReturnInvalid);
if (unicode_codepoints.size() == 1 && unicode_codepoints[0] == kInvalidUTF8) {
return token;
}
std::string decoded;
for (auto unicode_codepoint : unicode_codepoints) {
TVM_FFI_ICHECK(unicode_codepoint >= 0);
if (unicode_codepoint >= static_cast<int>(char_to_byte_map.size()) ||
char_to_byte_map[unicode_codepoint] == -1) {
// If there is no mapping, return the original token
return token;
}
decoded += static_cast<char>(char_to_byte_map[unicode_codepoint]);
}
return decoded;
}
/*!
* \brief Post-process a raw token to the actual token with the given post-processing method.
*/
inline std::string PostProcessToken(const std::string& token,
const std::string& token_postproc_method) {
if (token_postproc_method == "byte_fallback") {
return SpaceReplacerDecoder(ByteFallbackDecoder(token));
} else if (token_postproc_method == "byte_level") {
return ByteLevelDecoder(token);
} else {
LOG(FATAL) << "Unknown post-processing method: " << token_postproc_method;
}
}
std::vector<std::string> Tokenizer::PostProcessTokenTable(
const std::vector<std::string>& token_table, const std::string& token_postproc_method) {
std::vector<std::string> post_processed_token_table;
post_processed_token_table.reserve(token_table.size());
for (const std::string& token : token_table) {
post_processed_token_table.push_back(PostProcessToken(token, token_postproc_method));
}
return post_processed_token_table;
}
#ifndef COMPILE_MLC_WASM_RUNTIME
const std::vector<std::string>& TokenizerObj::PostProcessedTokenTable() {
if (!post_processed_token_table_.empty()) {
return post_processed_token_table_;
}
std::vector<std::string> raw_token_table;
int vocab_size = tokenizer->GetVocabSize();
raw_token_table.reserve(vocab_size);
for (int32_t token_id = 0; token_id < vocab_size; ++token_id) {
raw_token_table.push_back(tokenizer->IdToToken(token_id));
}
post_processed_token_table_ =
Tokenizer::PostProcessTokenTable(raw_token_table, info_->token_postproc_method);
return post_processed_token_table_;
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("mlc.tokenizers.Tokenizer", [](const String& path) { return Tokenizer::FromPath(path); })
.def("mlc.tokenizers.TokenizerEncode",
[](const Tokenizer& tokenizer, const String& text) {
std::vector<int32_t> token_ids = tokenizer->Encode(text);
return Shape{token_ids.begin(), token_ids.end()};
})
.def("mlc.tokenizers.TokenizerEncodeBatch",
[](const Tokenizer& tokenizer, const Array<String>& texts) {
std::vector<std::vector<int32_t>> results = tokenizer->EncodeBatch(texts);
Array<Shape> ret;
ret.reserve(results.size());
for (const auto& result : results) {
ret.push_back(Shape{result.begin(), result.end()});
}
return ret;
})
.def("mlc.tokenizers.TokenizerDecode",
[](const Tokenizer& tokenizer, const Shape& token_ids) {
return tokenizer->Decode({token_ids->data, token_ids->data + token_ids->size});
})
.def("mlc.tokenizers.DetectTokenizerInfo",
[](const String& path) { return Tokenizer::DetectTokenizerInfo(path)->AsJSONString(); });
}
#endif
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def_packed("mlc.tokenizers.PostProcessTokenTable",
[](tvm::ffi::PackedArgs args, tvm::ffi::Any* rv) {
Array<String> token_table_arr = args[0].cast<Array<String>>();
std::string token_postproc_method = args[args.size() - 1].cast<String>();
std::vector<std::string> token_table;
for (int i = 0; i < token_table_arr.size(); ++i) {
token_table.push_back(token_table_arr[i]);
}
std::vector<std::string> processed_token_table =
Tokenizer::PostProcessTokenTable(token_table, token_postproc_method);
// Convert std::vector<std::string> to Array<String>
Array<String> processed_token_table_tvm;
for (int i = 0; i < processed_token_table.size(); ++i) {
processed_token_table_tvm.push_back(processed_token_table[i]);
}
*rv = processed_token_table_tvm;
})
.def("mlc.tokenizers.PostProcessToken",
[](const String& token, const String& token_postproc_method) {
return PostProcessToken(token, token_postproc_method);
});
}
} // namespace llm
} // namespace mlc
+171
View File
@@ -0,0 +1,171 @@
/*!
* Copyright (c) 2023-2025 by Contributors
* \file tokenizers.h
* \brief Header of tokenizer related functions.
*/
#ifndef MLC_LLM_TOKENIZER_H_
#define MLC_LLM_TOKENIZER_H_
#include <tokenizers_cpp.h>
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
#include <optional>
#include <unordered_map>
#include "../base.h"
#include "../support/dynamic_bitset.h"
namespace mlc {
namespace llm {
using namespace tvm::runtime;
using tvm::ffi::Array;
using tvm::ffi::Object;
using tvm::ffi::ObjectPtr;
using tvm::ffi::ObjectRef;
using tvm::ffi::Shape;
using tvm::ffi::String;
/*! \brief Useful information of the tokenizer during generation. */
class TokenizerInfoNode : public Object {
public:
/*! \brief The method to post-process the tokens to their original strings.
* Possible values (each refers to a kind of tokenizer):
* - "byte_fallback": The same as the byte-fallback BPE tokenizer, including LLaMA-2,
* Mixtral-7b, etc. E.g. "▁of" -> " of", "<0x1B>" -> "\x1B".
* This method:
* 1) Transform tokens like <0x1B> to hex char byte 1B. (so-called byte-fallback)
* 2) Replace \\u2581 "▁" with space.
* - "byte_level": The same as the byte-level BPE tokenizer, including LLaMA-3, GPT-2,
* Phi-2, etc. E.g. "Ġin" -> " in", "ě" -> "\x1B"
* This method inverses the bytes-to-unicode transformation in the encoding process in
* https://github.com/huggingface/transformers/blob/87be06ca77166e6a6215eee5a990ab9f07238a18/src/transformers/models/gpt2/tokenization_gpt2.py#L38-L59
*/
String token_postproc_method = "byte_fallback";
/*! \brief Whether to prepend a space during encoding. */
bool prepend_space_in_encode = false;
/*! \brief Whether to strip the first space during decoding. */
bool strip_space_in_decode = false;
String AsJSONString() const;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<TokenizerInfoNode>();
}
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO("mlc.serve.TokenizerInfo", TokenizerInfoNode, Object);
};
class TokenizerInfo : public ObjectRef {
public:
/*! \brief Create a TokenizerInfo object from a dumped string. */
static TokenizerInfo FromJSONString(String json_string);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TokenizerInfo, ObjectRef, TokenizerInfoNode);
};
/*! \brief A wrapper object class for tokenizer. */
class TokenizerObj : public Object {
public:
/*! \brief The underlying tokenizer. */
std::unique_ptr<tokenizers::Tokenizer> tokenizer;
/*! \brief Encode text into ids. */
std::vector<int32_t> Encode(const std::string& text) const;
/*! \brief Encode text into ids. Some tokenizers may prepend a space in encoding, this method
* guarantees the space is not prepended. */
std::vector<int32_t> EncodeNoPrependSpace(const std::string& text) const;
/*! \brief Encode texts into ids. */
std::vector<std::vector<int32_t>> EncodeBatch(const Array<String>& texts) const;
/*! \brief Decode token ids into text. */
std::string Decode(const std::vector<int32_t>& token_ids) const;
/*! \brief Return the post-processed token table of the tokenizer. Special tokens are included. */
const std::vector<std::string>& PostProcessedTokenTable();
/*! \brief Get the prefix token mask as a bitset. The tokens which is a prefix of another token
* are set to true, and others are set to false in the bitset. */
const DynamicBitset& GetPrefixTokenMask();
/*!
* \brief Returns the vocabulary size. Special tokens are considered. This may be smaller than the
* `vocab_size` in config.json (length of logits), see https://github.com/QwenLM/Qwen2/issues/147
* and https://huggingface.co/microsoft/Phi-3-mini-4k-instruct/discussions/47.
*/
size_t GetVocabSize() const;
/*!
* \brief Convert the given id to its corresponding token if it exists. If not, return an
* empty string.
*/
std::string IdToToken(int32_t token_id) const;
/*!
* \brief Convert the given token to its corresponding id if it exists. If not, return -1.
*/
int32_t TokenToId(const std::string& token) const;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<TokenizerObj>();
}
friend class Tokenizer;
static constexpr const bool _type_has_method_sequal_reduce = false;
static constexpr const bool _type_has_method_shash_reduce = false;
static constexpr const bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("mlc.Tokenizer", TokenizerObj, Object);
private:
/*! \brief Useful information of the tokenizer during generation. */
TokenizerInfo info_;
/*! \brief The cached token table. */
std::vector<std::string> post_processed_token_table_;
/*! \brief The cached prefix token mask. */
DynamicBitset prefix_token_mask_;
};
class Tokenizer : public ObjectRef {
public:
/*!
* \brief Create a tokenizer from a directory path on disk.
* \param path The path to the tokenizer or the tokenizer directory.
* \param info The tokenizer info. If not provided, the info will be detected automatically.
*/
MLC_LLM_DLL static Tokenizer FromPath(const String& path,
std::optional<TokenizerInfo> info = std::nullopt);
/*! \brief Detect the tokenizer info from the given path of the tokenizer. */
MLC_LLM_DLL static TokenizerInfo DetectTokenizerInfo(const String& path);
/*!
* \brief Post-process the token table to their original strings.
* \param token_table The raw token table.
* \param postproc_method The postprocessing method to use.
* \returns The postprocessed token table containing the original strings.
*/
static std::vector<std::string> PostProcessTokenTable(const std::vector<std::string>& token_table,
const std::string& token_postproc_method);
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(Tokenizer, ObjectRef, TokenizerObj);
private:
explicit Tokenizer(std::unique_ptr<tokenizers::Tokenizer> tokenizer, TokenizerInfo info);
};
} // namespace llm
} // namespace mlc
#endif // MLC_LLM_TOKENIZER_H_