* common, grammar: add recursive depth limit, make it a requirement * catch more recursive functions * common : tidy up peg parser changes * grammar : reject candidates in a loop, without a depth limit The depth limit in llama_grammar_reject_candidates_for_stack counted the code points of a vocab token, so any token of 256 chars or more was always rejected under a grammar, such as the 512-space token of GLM-4.7-Flash or the long space tokens of Laguna-S-2.1. The rejection now walks the code points with its own stack of levels instead of recursing: there is no limit and no stack risk, whatever the length of a token from the GGUF, and the masks match master. * grammar : track rejected candidates as indices into the input At a given code point a candidate only differs from its input by the position of its code point pointer, so the levels now carry indices into the input candidates instead of copies. The first level needs no special case and the rejects map back to the input directly. Grammar sampling gets faster than master on GLM-4.7, Laguna, Qwen3.6 and gemma-4 with the json, list and spaces grammars, same masks. * nit : shorten the comment of llama_grammar_reject_step * common : add depth_guard for schema walks * grammar : move GRAMMAR_RECURSIVE_LIMIT into the .cpp * common : reduce recursion limit for peg parser * improve --------- Co-authored-by: Alde Rojas <hello@alde.dev> Co-authored-by: Pascal <admin@serveurperso.com>
152 lines
5.1 KiB
C++
152 lines
5.1 KiB
C++
#pragma once
|
|
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
#define JINJA_RECURSIVE_LIMIT 128
|
|
|
|
namespace jinja {
|
|
|
|
static void string_replace_all(std::string & s, const std::string & search, const std::string & replace) {
|
|
if (search.empty()) {
|
|
return;
|
|
}
|
|
std::string builder;
|
|
builder.reserve(s.length());
|
|
size_t pos = 0;
|
|
size_t last_pos = 0;
|
|
while ((pos = s.find(search, last_pos)) != std::string::npos) {
|
|
builder.append(s, last_pos, pos - last_pos);
|
|
builder.append(replace);
|
|
last_pos = pos + search.length();
|
|
}
|
|
builder.append(s, last_pos, std::string::npos);
|
|
s = std::move(builder);
|
|
}
|
|
|
|
// for displaying source code around error position
|
|
static std::string peak_source(const std::string & source, size_t pos, size_t max_peak_chars = 40) {
|
|
if (source.empty()) {
|
|
return "(no source available)";
|
|
}
|
|
std::string output;
|
|
size_t start = (pos >= max_peak_chars) ? (pos - max_peak_chars) : 0;
|
|
size_t end = std::min(pos + max_peak_chars, source.length());
|
|
std::string substr = source.substr(start, end - start);
|
|
string_replace_all(substr, "\n", "↵");
|
|
output += "..." + substr + "...\n";
|
|
std::string spaces(pos - start + 3, ' ');
|
|
output += spaces + "^";
|
|
return output;
|
|
}
|
|
|
|
static std::string fmt_error_with_source(const std::string & tag, const std::string & msg, const std::string & source, size_t pos) {
|
|
std::ostringstream oss;
|
|
oss << tag << ": " << msg << "\n";
|
|
oss << peak_source(source, pos);
|
|
return oss.str();
|
|
}
|
|
|
|
// Note: this is a simple hasher, not cryptographically secure, just for hash table usage
|
|
struct hasher {
|
|
static constexpr auto size_t_digits = sizeof(size_t) * 8;
|
|
static constexpr size_t prime = size_t_digits == 64 ? 0x100000001b3 : 0x01000193;
|
|
static constexpr size_t seed = size_t_digits == 64 ? 0xcbf29ce484222325 : 0x811c9dc5;
|
|
static constexpr auto block_size = sizeof(size_t); // in bytes; allowing the compiler to vectorize the computation
|
|
|
|
static_assert(size_t_digits == 64 || size_t_digits == 32);
|
|
static_assert(block_size == 8 || block_size == 4);
|
|
|
|
uint8_t buffer[block_size];
|
|
size_t idx = 0; // current index in buffer
|
|
size_t state = seed;
|
|
|
|
hasher() = default;
|
|
hasher(const std::type_info & type_inf) noexcept {
|
|
const auto type_hash = type_inf.hash_code();
|
|
update(&type_hash, sizeof(type_hash));
|
|
}
|
|
|
|
// Properties:
|
|
// - update is not associative: update(a).update(b) != update(b).update(a)
|
|
// - update(a ~ b) == update(a).update(b) with ~ as concatenation operator --> useful for streaming
|
|
// - update("", 0) --> state unchanged with empty input
|
|
hasher& update(void const * bytes, size_t len) noexcept {
|
|
const uint8_t * c = static_cast<uint8_t const *>(bytes);
|
|
if (len == 0) {
|
|
return *this;
|
|
}
|
|
size_t processed = 0;
|
|
|
|
// first, fill the existing buffer if it's partial
|
|
if (idx > 0) {
|
|
size_t to_fill = block_size - idx;
|
|
if (to_fill > len) {
|
|
to_fill = len;
|
|
}
|
|
std::memcpy(buffer + idx, c, to_fill);
|
|
idx += to_fill;
|
|
processed += to_fill;
|
|
if (idx == block_size) {
|
|
update_block(buffer);
|
|
idx = 0;
|
|
}
|
|
}
|
|
|
|
// process full blocks from the remaining input
|
|
for (; processed + block_size <= len; processed += block_size) {
|
|
update_block(c + processed);
|
|
}
|
|
|
|
// buffer any remaining bytes
|
|
size_t remaining = len - processed;
|
|
if (remaining > 0) {
|
|
std::memcpy(buffer, c + processed, remaining);
|
|
idx = remaining;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
// convenience function for testing only
|
|
hasher& update(const std::string & s) noexcept {
|
|
return update(s.data(), s.size());
|
|
}
|
|
|
|
// finalize and get the hash value
|
|
// note: after calling digest, the hasher state is modified, do not call update() again
|
|
size_t digest() noexcept {
|
|
// if there are remaining bytes in buffer, fill the rest with zeros and process
|
|
if (idx > 0) {
|
|
for (size_t i = idx; i < block_size; ++i) {
|
|
buffer[i] = 0;
|
|
}
|
|
update_block(buffer);
|
|
idx = 0;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
private:
|
|
// IMPORTANT: block must have at least block_size bytes
|
|
void update_block(const uint8_t * block) noexcept {
|
|
size_t blk = static_cast<uint32_t>(block[0])
|
|
| (static_cast<uint32_t>(block[1]) << 8)
|
|
| (static_cast<uint32_t>(block[2]) << 16)
|
|
| (static_cast<uint32_t>(block[3]) << 24);
|
|
if constexpr (block_size == 8) {
|
|
blk = blk | (static_cast<uint64_t>(block[4]) << 32)
|
|
| (static_cast<uint64_t>(block[5]) << 40)
|
|
| (static_cast<uint64_t>(block[6]) << 48)
|
|
| (static_cast<uint64_t>(block[7]) << 56);
|
|
}
|
|
state ^= blk;
|
|
state *= prime;
|
|
}
|
|
};
|
|
|
|
} // namespace jinja
|