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

371 lines
17 KiB
C++

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