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

443 lines
18 KiB
C++

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