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

453 lines
23 KiB
C++

/*!
* 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