#include "tokenizer.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace curriculum { namespace { bool is_hex(char c) { return std::isxdigit(static_cast(c)) != 0; } int hex_value(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } std::string vocab_string_to_bytes(const std::string& display) { std::string result; result.reserve(display.size()); size_t i = 0; while (i < display.size()) { if (i + 5 < display.size() && display[i] == '<' && display[i + 1] == '0' && display[i + 2] == 'x' && is_hex(display[i + 3]) && is_hex(display[i + 4]) && display[i + 5] == '>') { const int value = hex_value(display[i + 3]) * 16 + hex_value(display[i + 4]); result.push_back(static_cast(value)); i += 6; } else { result.push_back(display[i]); ++i; } } return result; } std::string bytes_to_vocab_string(const std::string& bytes) { std::string result; result.reserve(bytes.size()); for (unsigned char c : bytes) { if (c >= 0x20 && c <= 0x7E && c != static_cast('<')) { result.push_back(static_cast(c)); } else { char buffer[7]; std::snprintf(buffer, sizeof(buffer), "<0x%02X>", c); result += buffer; } } return result; } void append_utf8(std::string& out, uint32_t cp) { if (cp <= 0x7F) { out.push_back(static_cast(cp)); } else if (cp <= 0x7FF) { out.push_back(static_cast(0xC0 | (cp >> 6))); out.push_back(static_cast(0x80 | (cp & 0x3F))); } else if (cp <= 0xFFFF) { out.push_back(static_cast(0xE0 | (cp >> 12))); out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); out.push_back(static_cast(0x80 | (cp & 0x3F))); } else { out.push_back(static_cast(0xF0 | (cp >> 18))); out.push_back(static_cast(0x80 | ((cp >> 12) & 0x3F))); out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); out.push_back(static_cast(0x80 | (cp & 0x3F))); } } bool parse_hex4(const std::string& input, size_t pos, uint32_t& value) { if (pos + 4 > input.size()) return false; value = 0; for (size_t j = 0; j < 4; ++j) { const int h = hex_value(input[pos + j]); if (h < 0) return false; value = (value << 4) | static_cast(h); } return true; } bool parse_json_string( const std::string& content, size_t& i, std::string& output ) { if (i >= content.size() || content[i] != '"') return false; ++i; output.clear(); while (i < content.size()) { const char c = content[i++]; if (c == '"') return true; if (c != '\\') { output.push_back(c); continue; } if (i >= content.size()) return false; const char esc = content[i++]; switch (esc) { case '"': output.push_back('"'); break; case '\\': output.push_back('\\'); break; case '/': output.push_back('/'); break; case 'b': output.push_back('\b'); break; case 'f': output.push_back('\f'); break; case 'n': output.push_back('\n'); break; case 'r': output.push_back('\r'); break; case 't': output.push_back('\t'); break; case 'u': { uint32_t cp = 0; if (!parse_hex4(content, i, cp)) { return false; } i += 4; // Handle a UTF-16 surrogate pair when present. if (cp >= 0xD800 && cp <= 0xDBFF && i + 6 <= content.size() && content[i] == '\\' && content[i + 1] == 'u') { uint32_t low = 0; if (parse_hex4(content, i + 2, low) && low >= 0xDC00 && low <= 0xDFFF) { cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00); i += 6; } } append_utf8(output, cp); break; } default: return false; } } return false; } void skip_json_space(const std::string& content, size_t& i) { while (i < content.size() && (content[i] == ' ' || content[i] == '\n' || content[i] == '\r' || content[i] == '\t')) { ++i; } } bool parse_vocab_json( const std::string& content, std::unordered_map& output ) { size_t i = 0; skip_json_space(content, i); if (i >= content.size() || content[i++] != '{') { return false; } while (true) { skip_json_space(content, i); if (i < content.size() && content[i] == '}') { return true; } std::string key; if (!parse_json_string(content, i, key)) { return false; } skip_json_space(content, i); if (i >= content.size() || content[i++] != ':') { return false; } skip_json_space(content, i); if (i >= content.size() || !std::isdigit(static_cast(content[i]))) { return false; } int64_t value = 0; while (i < content.size() && std::isdigit(static_cast(content[i]))) { value = value * 10 + (content[i++] - '0'); if (value > std::numeric_limits::max()) { return false; } } output.emplace( std::move(key), static_cast(value) ); skip_json_space(content, i); if (i < content.size() && content[i] == ',') { ++i; continue; } if (i < content.size() && content[i] == '}') { return true; } return false; } } struct CodePoint { uint32_t value; size_t length; }; CodePoint decode_utf8(std::string_view text, size_t i) { const auto first = static_cast(text[i]); if (first < 0x80) { return {first, 1}; } auto continuation = [&](size_t offset) { return i + offset < text.size() && (static_cast(text[i + offset]) & 0xC0) == 0x80; }; if ((first & 0xE0) == 0xC0 && continuation(1)) { return { static_cast((first & 0x1F) << 6) | (static_cast(text[i + 1]) & 0x3F), 2 }; } if ((first & 0xF0) == 0xE0 && continuation(1) && continuation(2)) { return { static_cast((first & 0x0F) << 12) | static_cast( (static_cast(text[i + 1]) & 0x3F) << 6 ) | (static_cast(text[i + 2]) & 0x3F), 3 }; } if ((first & 0xF8) == 0xF0 && continuation(1) && continuation(2) && continuation(3)) { return { static_cast((first & 0x07) << 18) | static_cast( (static_cast(text[i + 1]) & 0x3F) << 12 ) | static_cast( (static_cast(text[i + 2]) & 0x3F) << 6 ) | (static_cast(text[i + 3]) & 0x3F), 4 }; } // Invalid UTF-8: preserve the byte as its own token. return {first, 1}; } bool is_shared_whitespace(uint32_t cp) { return (cp >= 0x0009 && cp <= 0x000D) || cp == 0x0020 || cp == 0x0085 || cp == 0x00A0 || cp == 0x1680 || (cp >= 0x2000 && cp <= 0x200A) || cp == 0x2028 || cp == 0x2029 || cp == 0x202F || cp == 0x205F || cp == 0x3000; } constexpr size_t kMaxNonWhitespaceBytes = 256; uint64_t pair_key(int32_t left, int32_t right) { return ( static_cast( static_cast(left) ) << 32 ) | static_cast(right); } constexpr uint32_t kNoNode = std::numeric_limits::max(); struct MergeHeapEntry { size_t rank; uint32_t left_node; uint64_t generation; }; struct MergeHeapEntryGreater { bool operator()( const MergeHeapEntry& a, const MergeHeapEntry& b ) const { if (a.rank != b.rank) { return a.rank > b.rank; } return a.left_node > b.left_node; } }; } // namespace struct Tokenizer::EncodingScratch { std::vector tokens, encoded, legacy_next; std::vector previous, next; std::vector generation; std::vector heap; }; bool Tokenizer::load( const std::string& vocab_path, const std::string& merges_path ) { std::unique_lock lock(model_mutex_); invalidate_cache(); id_to_token_.clear(); token_to_id_.clear(); merges_.clear(); merge_lookup_.clear(); fast_merge_order_ = true; special_tokens_.clear(); special_token_aliases_.clear(); if (!load_vocab(vocab_path)) { std::cerr << "Failed to load vocab from " << vocab_path << '\n'; return false; } if (!load_merges(merges_path)) { std::cerr << "Failed to load merges from " << merges_path << '\n'; return false; } return true; } bool Tokenizer::load_vocab(const std::string& path) { std::ifstream file(path, std::ios::binary); if (!file) { return false; } const std::string content( (std::istreambuf_iterator(file)), std::istreambuf_iterator() ); std::unordered_map raw_vocab; if (!parse_vocab_json(content, raw_vocab) || raw_vocab.empty()) { return false; } int32_t max_id = -1; for (const auto& entry : raw_vocab) { max_id = std::max( max_id, entry.second ); } id_to_token_.assign( static_cast(max_id) + 1, std::string{} ); std::vector seen_ids( id_to_token_.size(), false ); for (const auto& [display, id] : raw_vocab) { if (id < 0 || static_cast(id) >= id_to_token_.size()) { return false; } const std::string bytes = vocab_string_to_bytes(display); if (bytes.empty()) { std::cerr << "Empty token in vocabulary at ID " << id << '\n'; return false; } if (seen_ids[static_cast(id)]) { std::cerr << "Duplicate vocabulary ID " << id << '\n'; return false; } if (token_to_id_.find(bytes) != token_to_id_.end()) { std::cerr << "Duplicate vocabulary token bytes at ID " << id << '\n'; return false; } seen_ids[static_cast(id)] = true; id_to_token_[id] = bytes; token_to_id_[bytes] = id; } for (size_t id = 0; id < seen_ids.size(); ++id) { if (!seen_ids[id]) { std::cerr << "Missing vocabulary ID " << id << '\n'; return false; } } for (int32_t id = 0; id < 256; ++id) { if (static_cast(id) >= id_to_token_.size()) { return false; } const std::string expected( 1, static_cast(id) ); if (id_to_token_[id] != expected) { std::cerr << "Invalid base-byte token at ID " << id << '\n'; return false; } } return true; } bool Tokenizer::load_merges( const std::string& path ) { std::ifstream file(path); if (!file) { return false; } merge_lookup_.clear(); fast_merge_order_ = true; int32_t previous_result = 255; std::string line; size_t line_number = 0; while (std::getline(file, line)) { ++line_number; if (line.empty() || line[0] == '#') { continue; } int64_t left = -1; int64_t right = -1; int64_t result = -1; std::istringstream stream(line); std::string extra; if (!(stream >> left >> right >> result) || (stream >> extra)) { std::cerr << "Invalid merge rule at line " << line_number << '\n'; return false; } const auto valid_id = [&](int64_t id) { return id >= 0 && static_cast(id) < id_to_token_.size(); }; if (!valid_id(left) || !valid_id(right) || !valid_id(result)) { std::cerr << "Out-of-range merge ID at line " << line_number << '\n'; return false; } const std::string expected = id_to_token_[left] + id_to_token_[right]; if (id_to_token_[result] != expected) { std::cerr << "Merge result bytes do not match at line " << line_number << '\n'; return false; } const int32_t left_id = static_cast(left); const int32_t right_id = static_cast(right); const int32_t result_id = static_cast(result); const size_t rank = merges_.size(); // A trained vocabulary assigns monotonically increasing IDs to merge // results. That makes newly-created pairs strictly later rules, which // is the condition needed by the indexed heap encoder below. Keep the // legacy path for unusual hand-authored files. if (result_id <= previous_result || result_id <= left_id || result_id <= right_id) { fast_merge_order_ = false; } previous_result = result_id; const auto [_, inserted] = merge_lookup_.emplace( pair_key(left_id, right_id), MergeInfo{rank, result_id} ); if (!inserted) { fast_merge_order_ = false; } merges_.emplace_back( left_id, right_id, result_id ); } return true; } void Tokenizer::encode_word_into(std::string_view bytes, EncodingScratch& scratch) const { auto& tokens = scratch.tokens; auto& encoded = scratch.encoded; tokens.resize(bytes.size()); // char may be signed; byte IDs must remain in [0, 255]. for (size_t i = 0; i < bytes.size(); ++i) tokens[i] = static_cast(bytes[i]); encoded.clear(); if (tokens.size() < 2 || merges_.empty()) { encoded.assign(tokens.begin(), tokens.end()); return; } if (!fast_merge_order_) { auto& next = scratch.legacy_next; for (const auto& [left, right, result] : merges_) { next.clear(); for (size_t i = 0; i < tokens.size();) { if (i + 1 < tokens.size() && tokens[i] == left && tokens[i + 1] == right) { next.push_back(result); i += 2; } else next.push_back(tokens[i++]); } tokens.swap(next); } encoded.assign(tokens.begin(), tokens.end()); return; } if (tokens.size() == 2) { const auto found = merge_lookup_.find(pair_key(tokens[0], tokens[1])); if (found != merge_lookup_.end()) encoded.push_back(found->second.result); else encoded.assign(tokens.begin(), tokens.end()); return; } if (tokens.size() >= kNoNode) throw std::length_error("pretoken piece exceeds node index range"); const size_t count = tokens.size(); auto& previous = scratch.previous; auto& next = scratch.next; auto& generation = scratch.generation; auto& heap = scratch.heap; previous.assign(count, kNoNode); next.assign(count, kNoNode); generation.assign(count, 0); heap.clear(); for (uint32_t i = 0; i + 1 < count; ++i) { next[i] = i + 1; previous[i + 1] = i; } const size_t no_rank = std::numeric_limits::max(); bool heap_ready = false; auto enqueue = [&](uint32_t left, size_t completed_rank) { if (left == kNoNode || next[left] == kNoNode) return; const auto found = merge_lookup_.find(pair_key(tokens[left], tokens[next[left]])); if (found == merge_lookup_.end() || (completed_rank != no_rank && found->second.rank <= completed_rank)) return; heap.push_back({found->second.rank, left, generation[left]}); if (heap_ready) std::push_heap(heap.begin(), heap.end(), MergeHeapEntryGreater{}); }; for (uint32_t i = 0; i + 1 < count; ++i) enqueue(i, no_rank); std::make_heap(heap.begin(), heap.end(), MergeHeapEntryGreater{}); heap_ready = true; auto pop = [&]() { std::pop_heap(heap.begin(), heap.end(), MergeHeapEntryGreater{}); const auto entry = heap.back(); heap.pop_back(); return entry; }; size_t completed_rank = no_rank; while (!heap.empty()) { const auto first = pop(); if (completed_rank != no_rank && first.rank <= completed_rank) continue; const size_t rank = first.rank; completed_rank = rank; auto merge_if_current = [&](const MergeHeapEntry& entry) { if (entry.left_node >= count || generation[entry.left_node] != entry.generation) return; const uint32_t left = entry.left_node; const uint32_t right = next[left]; if (right == kNoNode) return; const auto found = merge_lookup_.find(pair_key(tokens[left], tokens[right])); if (found == merge_lookup_.end() || found->second.rank != rank) return; const uint32_t before = previous[left]; const uint32_t after = next[right]; tokens[left] = found->second.result; next[left] = after; if (after != kNoNode) previous[after] = left; next[right] = previous[right] = kNoNode; ++generation[left]; ++generation[right]; if (before != kNoNode) ++generation[before]; enqueue(before, rank); enqueue(left, rank); }; merge_if_current(first); while (!heap.empty() && heap.front().rank == rank) merge_if_current(pop()); } for (uint32_t node = 0; node != kNoNode; node = next[node]) encoded.push_back(tokens[node]); } void Tokenizer::append_word(std::string_view bytes, std::vector& output, EncodingScratch& scratch) const { if (bytes.size() == 1) { output.push_back(static_cast(bytes[0])); return; } if (cache_capacity_ == 0 || bytes.size() > kMaxCachedPieceBytes) { encode_word_into(bytes, scratch); output.insert(output.end(), scratch.encoded.begin(), scratch.encoded.end()); return; } const size_t shard_id = std::hash{}(bytes) % kCacheShards; const size_t capacity = cache_capacity_ / kCacheShards + (shard_id < cache_capacity_ % kCacheShards); const bool cacheable = capacity != 0; auto& shard = piece_cache_[shard_id]; if (cacheable) { std::lock_guard lock(shard.mutex); const auto found = shard.index.find(bytes); if (found != shard.index.end()) { ++shard.hits; shard.lru.splice(shard.lru.begin(), shard.lru, found->second); const auto& ids = found->second->ids; output.insert(output.end(), ids.begin(), ids.end()); return; } ++shard.misses; } encode_word_into(bytes, scratch); const auto& ids = scratch.encoded; if (cacheable) { std::lock_guard lock(shard.mutex); // Another encoding thread can have filled this exact piece meanwhile. if (shard.index.find(bytes) == shard.index.end()) { shard.lru.push_front({std::string(bytes), ids}); try { shard.index.emplace(std::string_view(shard.lru.front().bytes), shard.lru.begin()); } catch (...) { shard.lru.pop_front(); throw; } shard.payload_bytes += bytes.size() + ids.size() * sizeof(int32_t); while (shard.lru.size() > capacity || shard.payload_bytes > kCachePayloadBytes / kCacheShards) { const auto last = std::prev(shard.lru.end()); shard.payload_bytes -= last->bytes.size() + last->ids.size() * sizeof(int32_t); shard.index.erase(std::string_view(last->bytes)); shard.lru.erase(last); } } } output.insert(output.end(), ids.begin(), ids.end()); } void Tokenizer::append_regular(std::string_view text, std::vector& output, EncodingScratch& scratch) const { size_t i = 0; while (i < text.size()) { const size_t start = i; while (i < text.size()) { const auto cp = decode_utf8(text, i); if (!is_shared_whitespace(cp.value)) break; i += cp.length; } const size_t field_start = i; while (i < text.size()) { const auto cp = decode_utf8(text, i); if (is_shared_whitespace(cp.value) || (i > field_start && i + cp.length - field_start > kMaxNonWhitespaceBytes)) break; i += cp.length; } append_word(text.substr(start, i - start), output, scratch); } } std::vector Tokenizer::encode_impl(const std::string& text) const { // Scratch holds only temporary arrays; cached results belong to the model. thread_local EncodingScratch scratch; std::vector output; output.reserve(text.size() / 3 + 8); const std::string_view view(text); if (special_tokens_.empty()) append_regular(view, output, scratch); else { size_t cursor = 0; while (cursor < text.size()) { size_t position = std::string::npos, length = 0; int32_t id = -1; for (const auto& special : special_tokens_) { const size_t found = text.find(special.first, cursor); if (found < position || (found != std::string::npos && found == position && special.first.size() > length)) { position = found; length = special.first.size(); id = special.second; } } if (position == std::string::npos) { append_regular(view.substr(cursor), output, scratch); break; } append_regular(view.substr(cursor, position - cursor), output, scratch); output.push_back(id); cursor = position + length; } } // A giant whitespace run must not permanently pin a giant per-thread arena. if (scratch.tokens.capacity() > 4096 || scratch.encoded.capacity() > 4096 || scratch.legacy_next.capacity() > 4096 || scratch.heap.capacity() > 16384) scratch = EncodingScratch{}; return output; } std::vector Tokenizer::encode(const std::string& text) const { std::shared_lock lock(model_mutex_); return encode_impl(text); } void Tokenizer::invalidate_cache() { for (auto& shard : piece_cache_) { std::lock_guard lock(shard.mutex); shard.index.clear(); shard.lru.clear(); shard.payload_bytes = shard.hits = shard.misses = 0; } } void Tokenizer::set_cache_capacity(size_t capacity) { if (capacity > 1048576) throw std::invalid_argument("cache capacity exceeds 1048576 pieces"); std::unique_lock lock(model_mutex_); invalidate_cache(); cache_capacity_ = capacity; } void Tokenizer::clear_cache() { std::unique_lock lock(model_mutex_); invalidate_cache(); } std::unordered_map Tokenizer::cache_info() const { std::shared_lock model_lock(model_mutex_); std::unordered_map result{ {"capacity", cache_capacity_}, {"entries", 0}, {"hits", 0}, {"misses", 0}, {"payload_bytes", 0}, {"max_payload_bytes", kCachePayloadBytes}}; for (auto& shard : piece_cache_) { std::lock_guard lock(shard.mutex); result["entries"] += shard.index.size(); result["hits"] += shard.hits; result["misses"] += shard.misses; result["payload_bytes"] += shard.payload_bytes; } return result; } size_t Tokenizer::vocab_size() const { std::shared_lock lock(model_mutex_); return id_to_token_.size(); } std::string Tokenizer::decode( const std::vector& ids ) const { std::shared_lock lock(model_mutex_); std::string result; for (int32_t id : ids) { if (id < 0 || static_cast(id) >= id_to_token_.size()) { throw std::out_of_range( "token ID out of vocabulary range" ); } const auto alias = special_token_aliases_.find(id); result += alias == special_token_aliases_.end() ? id_to_token_[id] : alias->second; } return result; } std::vector> Tokenizer::encode_batch( const std::vector& texts, size_t num_threads ) const { if (num_threads > 64) throw std::invalid_argument("num_threads must be between 0 and 64"); std::shared_lock lock(model_mutex_); std::vector> result(texts.size()); size_t workers = num_threads; if (workers == 0) { size_t bytes = 0; for (const auto& text : texts) bytes += text.size(); workers = bytes >= 256 * 1024 ? std::min(4, std::max(1u, std::thread::hardware_concurrency())) : 1; } workers = std::min(workers, texts.size()); if (workers <= 1) { for (size_t i = 0; i < texts.size(); ++i) result[i] = encode_impl(texts[i]); return result; } std::atomic cursor{0}; std::atomic failed{false}; std::exception_ptr error; std::mutex error_mutex; auto work = [&]() { try { while (!failed.load(std::memory_order_relaxed)) { const size_t i = cursor.fetch_add(1, std::memory_order_relaxed); if (i >= texts.size()) break; result[i] = encode_impl(texts[i]); } } catch (...) { std::lock_guard error_lock(error_mutex); if (!error) error = std::current_exception(); failed.store(true, std::memory_order_relaxed); } }; std::vector threads; try { for (size_t i = 1; i < workers; ++i) threads.emplace_back(work); } catch (...) { failed.store(true, std::memory_order_relaxed); for (auto& thread : threads) thread.join(); throw; } work(); for (auto& thread : threads) thread.join(); if (error) std::rethrow_exception(error); return result; } std::string Tokenizer::id_to_token( int32_t id ) const { std::shared_lock lock(model_mutex_); if (id < 0 || static_cast(id) >= id_to_token_.size()) { return {}; } const auto alias = special_token_aliases_.find(id); return bytes_to_vocab_string(alias == special_token_aliases_.end() ? id_to_token_[id] : alias->second); } int32_t Tokenizer::token_to_id( const std::string& token ) const { std::shared_lock lock(model_mutex_); const auto it = token_to_id_.find(token); return it == token_to_id_.end() ? -1 : it->second; } void Tokenizer::set_special_tokens( const std::vector& tokens ) { std::unique_lock lock(model_mutex_); std::unordered_map configured; for (const auto& token : tokens) { const auto found = token_to_id_.find(token); if (token.empty() || found == token_to_id_.end() || found->second < 256) { throw std::invalid_argument("special token is missing from the loaded vocabulary: " + token); } if (!configured.emplace(token, found->second).second) { throw std::invalid_argument("duplicate special token: " + token); } for (const auto& [left, right, result] : merges_) { if (left == found->second || right == found->second || result == found->second) { throw std::invalid_argument("special token ID participates in an ordinary merge: " + token); } } } invalidate_cache(); special_tokens_ = std::move(configured); special_token_aliases_.clear(); } void Tokenizer::configure_special_tokens( const std::unordered_map& token_ids, const std::unordered_map& aliases, size_t expected_vocab_size ) { std::unique_lock lock(model_mutex_); // Legacy artifacts may keep tail controls in metadata rather than vocab.json. // Validate every addition before committing; existing ordinary IDs are immutable. std::unordered_map additions; std::unordered_set protected_ids; size_t final_size = id_to_token_.size(); for (const auto& [token, id] : token_ids) { if (token.empty() || id < 256 || !protected_ids.insert(id).second) { throw std::invalid_argument("invalid or duplicate canonical special-token ID"); } const auto existing = token_to_id_.find(token); if (existing != token_to_id_.end() && existing->second != id) { throw std::invalid_argument("configured special-token ID disagrees with vocabulary"); } if (static_cast(id) < id_to_token_.size()) { if (id_to_token_[id] != token) { throw std::invalid_argument("special token would overwrite an existing vocabulary ID"); } } else { additions.emplace(id, token); } final_size = std::max(final_size, static_cast(id) + 1); } for (const auto& [id, token] : aliases) { if (id < 256 || token_ids.find(token) == token_ids.end() || !protected_ids.insert(id).second) { throw std::invalid_argument("invalid or conflicting decode-only alias"); } if (static_cast(id) >= id_to_token_.size()) { additions.emplace(id, token); } final_size = std::max(final_size, static_cast(id) + 1); } if (final_size != expected_vocab_size || final_size - id_to_token_.size() != additions.size()) { throw std::invalid_argument("configured vocabulary size disagrees with vocabulary and explicit controls"); } for (const auto& [left, right, result] : merges_) { if (protected_ids.count(left) || protected_ids.count(right) || protected_ids.count(result)) { throw std::invalid_argument("configured control or alias ID participates in an ordinary merge"); } } // Unique additions with the correct span count are necessarily contiguous. id_to_token_.reserve(final_size); for (size_t id = id_to_token_.size(); id < final_size; ++id) { id_to_token_.push_back(additions.at(static_cast(id))); } for (const auto& [token, id] : token_ids) { token_to_id_.emplace(token, id); } invalidate_cache(); special_tokens_ = token_ids; special_token_aliases_ = aliases; } void Tokenizer::set_special_token_aliases( const std::unordered_map& aliases ) { std::unique_lock lock(model_mutex_); for (const auto& [id, token] : aliases) { if (id < 256 || static_cast(id) >= id_to_token_.size() || special_tokens_.find(token) == special_tokens_.end()) { throw std::invalid_argument("invalid decode-only special-token alias"); } for (const auto& special : special_tokens_) { if (special.second == id) { throw std::invalid_argument("decode-only alias conflicts with a canonical special ID"); } } for (const auto& [left, right, result] : merges_) { if (left == id || right == id || result == id) { throw std::invalid_argument("decode-only alias would replace a learned token ID"); } } } invalidate_cache(); special_token_aliases_ = aliases; } } // namespace curriculum