/*! * Copyright (c) 2023-2025 by Contributors * \file serve/engine_actions/batch_verify.cc */ #include #include #include #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 models, Tokenizer tokenizer, Optional trace_recorder) : models_(std::move(models)), tokenizer_(tokenizer), trace_recorder_(std::move(trace_recorder)) {} Array 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 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(new_tokens.size()) - rollback_cnt, 0); rsentry->rstate->metrics.completion_tokens += static_cast(new_tokens.size()) - rollback_cnt; } auto tend = std::chrono::high_resolution_clock::now(); estate->metrics.engine_jump_forward_time_sum += static_cast((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, 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 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(past_tokens.size()); ++i) { if (i == static_cast(new_tokens.size()) || past_tokens[i] != new_tokens[i]) { first_differ_idx = i; break; } } return {past_tokens.size() - first_differ_idx, std::vector(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& new_tokens, const std::string& new_string) { // 1. Handle rollback for the stream output if (rollback_cnt > static_cast(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(mstate->committed_tokens.size()) - rollback_cnt + static_cast(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 models_; /*! \brief Tokenizer for retokenization. */ Tokenizer tokenizer_; /*! \brief Event trace recorder. */ Optional trace_recorder_; /*! \brief The maximum number of tokens to rollback. */ const int MAX_ROLLBACK_TOKENS_ = 10; }; EngineAction EngineAction::BatchJumpForward(Array models, Tokenizer tokenizer, Optional trace_recorder) { return EngineAction(tvm::ffi::make_object( std::move(models), std::move(tokenizer), std::move(trace_recorder))); } } // namespace serve } // namespace llm } // namespace mlc