mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-28 09:45:55 +08:00
llama: fix exhausting CPU and MEM caused by dry sequence breaker (#31035)
This commit is contained in:
1 parent
5c965fd8ca
commit
63412d8737
5 files changed
+98
-13
No files matched your search
@@ -1564,6 +1564,10 @@ extern "C" {
|
||||
float penalty_freq, // must be finite, 0.0 = disabled
|
||||
float penalty_present); // must be finite, 0.0 = disabled
|
||||
|
||||
/// maximum number of distinct DRY sequence breakers, duplicates not counted
|
||||
/// processing each breaker scans the entire vocabulary, so the init cost is O(num_breakers x n_vocab)
|
||||
#define LLAMA_DRY_MAX_SEQ_BREAKERS 64
|
||||
|
||||
/// @details DRY sampler, designed by p-e-w, as described in: https://github.com/oobabooga/text-generation-webui/pull/5677, porting Koboldcpp implementation authored by pi6am: https://github.com/LostRuins/koboldcpp/pull/982
|
||||
LLAMA_API struct llama_sampler * llama_sampler_init_dry(
|
||||
const struct llama_vocab * vocab,
|
||||
|
||||
+36
-12
@@ -18,6 +18,7 @@
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <stdexcept>
|
||||
|
||||
// the ring buffer works similarly to std::deque, but with a fixed capacity
|
||||
@@ -3333,9 +3334,13 @@ struct llama_sampler_dry {
|
||||
};
|
||||
|
||||
// Ported from Koboldcpp, original PR: https://github.com/LostRuins/koboldcpp/pull/982 (Original author: pi6am)
|
||||
static void get_overlapping_token_sequences(const llama_vocab & vocab, const std::string& str, std::unordered_multimap<llama_token, std::vector<llama_token>>& token_sequences, int max_tail_len = -1) {
|
||||
for (llama_token token_id = 0; token_id < (llama_token) vocab.n_tokens(); token_id++) {
|
||||
std::string word = vocab.detokenize({token_id}, true);
|
||||
static void get_overlapping_token_sequences(const llama_vocab & vocab, const std::vector<std::string> & vocab_words, const std::string& str, std::unordered_multimap<llama_token, std::vector<llama_token>>& token_sequences, int max_tail_len = -1) {
|
||||
// the tail tokenization only depends on how many characters of str were matched, so compute each one at most once
|
||||
std::vector<std::vector<llama_token>> tail_cache(str.size() + 1);
|
||||
std::vector<bool> tail_cached(str.size() + 1, false);
|
||||
|
||||
for (llama_token token_id = 0; token_id < (llama_token) vocab_words.size(); token_id++) {
|
||||
const std::string & word = vocab_words[token_id];
|
||||
if (word.find(str) != std::string::npos) {
|
||||
token_sequences.emplace(token_id, std::vector<llama_token>());
|
||||
} else {
|
||||
@@ -3352,10 +3357,14 @@ static void get_overlapping_token_sequences(const llama_vocab & vocab, const std
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
std::vector<llama_token> tokenization = vocab.tokenize(str.substr(i), false, false);
|
||||
if (max_tail_len >= 0 && tokenization.size() > (size_t)max_tail_len) {
|
||||
tokenization.resize(max_tail_len);
|
||||
if (!tail_cached[i]) {
|
||||
tail_cache[i] = vocab.tokenize(str.substr(i), false, false);
|
||||
if (max_tail_len >= 0 && tail_cache[i].size() > (size_t)max_tail_len) {
|
||||
tail_cache[i].resize(max_tail_len);
|
||||
}
|
||||
tail_cached[i] = true;
|
||||
}
|
||||
const std::vector<llama_token> & tokenization = tail_cache[i];
|
||||
|
||||
// Ensure we don't already have a duplicate matching tokenization
|
||||
auto its = token_sequences.equal_range(token_id);
|
||||
@@ -3656,6 +3665,16 @@ struct llama_sampler * llama_sampler_init_dry(const struct llama_vocab * vocab,
|
||||
|
||||
if (dry_enabled && seq_breakers != nullptr && num_breakers > 0) {
|
||||
// Process sequence breakers
|
||||
// each distinct breaker costs a full vocabulary scan: drop duplicates and stop at the cap
|
||||
std::unordered_set<std::string> seen_breakers;
|
||||
|
||||
// detokenize the vocabulary once instead of rescanning it for every breaker
|
||||
std::vector<std::string> vocab_words;
|
||||
vocab_words.reserve(vocab->n_tokens());
|
||||
for (llama_token token_id = 0; token_id < (llama_token) vocab->n_tokens(); ++token_id) {
|
||||
vocab_words.push_back(vocab->detokenize({token_id}, true));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num_breakers; ++i) {
|
||||
if (seq_breakers[i] == nullptr || std::strlen(seq_breakers[i]) == 0) {
|
||||
LLAMA_LOG_WARN("skipping null or empty DRY sequence breaker at index %zu\n", i);
|
||||
@@ -3663,17 +3682,22 @@ struct llama_sampler * llama_sampler_init_dry(const struct llama_vocab * vocab,
|
||||
}
|
||||
|
||||
std::string sequence_break(seq_breakers[i]);
|
||||
if (sequence_break.empty()) {
|
||||
LLAMA_LOG_WARN("skipping empty DRY sequence breaker\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sequence_break.size() > MAX_CHAR_LEN) {
|
||||
LLAMA_LOG_WARN("truncating DRY sequence breaker to %d characters\n", MAX_CHAR_LEN);
|
||||
sequence_break.resize(MAX_CHAR_LEN);
|
||||
}
|
||||
|
||||
get_overlapping_token_sequences(*vocab, sequence_break, processed_breakers, MAX_SEQ_LEN);
|
||||
if (!seen_breakers.insert(sequence_break).second) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seen_breakers.size() > LLAMA_DRY_MAX_SEQ_BREAKERS) {
|
||||
LLAMA_LOG_WARN("ignoring DRY sequence breaker at index %zu: more than %d distinct breakers\n",
|
||||
i, LLAMA_DRY_MAX_SEQ_BREAKERS);
|
||||
break;
|
||||
}
|
||||
|
||||
get_overlapping_token_sequences(*vocab, vocab_words, sequence_break, processed_breakers, MAX_SEQ_LEN);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "../tools/server/server-common.h"
|
||||
#include "../tools/server/server-decision.h"
|
||||
#include "../tools/server/server-schema.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <regex>
|
||||
@@ -490,6 +491,42 @@ static void test_decision_depth(testing & t) {
|
||||
});
|
||||
}
|
||||
|
||||
// DRY sequence breakers
|
||||
//
|
||||
|
||||
static void test_dry_sequence_breakers(testing & t) {
|
||||
common_params params_base;
|
||||
|
||||
const auto eval = [&](const json & data) {
|
||||
return server_schema::eval_llama_cmpl_schema(nullptr, params_base, {}, data);
|
||||
};
|
||||
|
||||
{
|
||||
json data;
|
||||
data["dry_sequence_breakers"] = json::array({"ab", "ab", "cd", "ab"});
|
||||
const auto params = eval(data);
|
||||
const std::vector<std::string> expected = {"ab", "cd"};
|
||||
t.assert_true("duplicates removed, order kept", params.sampling.dry_sequence_breakers == expected);
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<std::string> many;
|
||||
for (int i = 0; i <= LLAMA_DRY_MAX_SEQ_BREAKERS; ++i) {
|
||||
many.push_back("b" + std::to_string(i));
|
||||
}
|
||||
json data;
|
||||
data["dry_sequence_breakers"] = many;
|
||||
|
||||
bool thrown = false;
|
||||
try {
|
||||
eval(data);
|
||||
} catch (const std::exception &) {
|
||||
thrown = true;
|
||||
}
|
||||
t.assert_true("more than the cap is rejected", thrown);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
testing t;
|
||||
|
||||
@@ -511,6 +548,7 @@ int main(int argc, char ** argv) {
|
||||
t.test("decision_escape", test_decision_escape);
|
||||
t.test("rerank_template", test_rerank_template);
|
||||
t.test("decision_depth", test_decision_depth);
|
||||
t.test("dry_sequence_breakers", test_dry_sequence_breakers);
|
||||
|
||||
return t.summary();
|
||||
}
|
||||
@@ -563,7 +563,7 @@ These words will not be included in the completion, so make sure to add them to
|
||||
|
||||
`dry_penalty_last_n`: How many tokens to scan for repetitions. Default: `64`, where `0` is disabled.
|
||||
|
||||
`dry_sequence_breakers`: Specify an array of sequence breakers for DRY sampling. Only a JSON array of strings is accepted. Default: `['\n', ':', '"', '*']`
|
||||
`dry_sequence_breakers`: Specify an array of sequence breakers for DRY sampling. Only a JSON array of strings is accepted. At most 64 distinct breakers, duplicates are ignored. Default: `['\n', ':', '"', '*']`
|
||||
|
||||
`xtc_probability`: Set the chance for token removal via XTC sampler. Default: `0.0`, which is disabled.
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "json-schema-to-grammar.h"
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
namespace server_schema {
|
||||
|
||||
//
|
||||
@@ -246,6 +248,23 @@ std::vector<std::unique_ptr<field>> make_llama_cmpl_schema(const common_params &
|
||||
if (ctx.params.sampling.dry_sequence_breakers.empty()) {
|
||||
throw std::runtime_error("Error: dry_sequence_breakers must be a non-empty array of strings");
|
||||
}
|
||||
|
||||
// each distinct breaker costs a full vocabulary scan at sampler init, so dedupe before counting
|
||||
std::unordered_set<std::string> seen;
|
||||
std::vector<std::string> unique_breakers;
|
||||
|
||||
for (const auto & breaker : ctx.params.sampling.dry_sequence_breakers) {
|
||||
if (seen.insert(breaker).second) {
|
||||
unique_breakers.push_back(breaker);
|
||||
}
|
||||
}
|
||||
|
||||
if (unique_breakers.size() > LLAMA_DRY_MAX_SEQ_BREAKERS) {
|
||||
throw std::runtime_error(string_format(
|
||||
"Error: dry_sequence_breakers must contain at most %d distinct entries", LLAMA_DRY_MAX_SEQ_BREAKERS));
|
||||
}
|
||||
|
||||
ctx.params.sampling.dry_sequence_breakers = std::move(unique_breakers);
|
||||
}));
|
||||
|
||||
// handle both "json_schema" and "grammar"
|
||||
|
||||
Reference in new issue
Block a user