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

330 lines
14 KiB
C++

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