unicode : add opt-in NFC normalization for Unigram tokenizers (#30390)
* unicode : add opt-in NFC normalization for Unigram tokenizers Note: used help from Codex, Sol 6.1 Medium * Fixing initial bot comments + linter error (using existing out instead of print) * removed test_nfc_tokenizer
This commit is contained in:
1 parent
eca5458ce0
commit
10da8c463f
12 files changed
+4165
-7
No files matched your search
@@ -364,6 +364,7 @@ class Keys:
|
||||
CHAT_TEMPLATE_N = "tokenizer.chat_template.{name}"
|
||||
CHAT_TEMPLATES = "tokenizer.chat_templates"
|
||||
# Normalizer constants
|
||||
NORMALIZER_NFC = "tokenizer.ggml.normalizer.nfc"
|
||||
NORMALIZER_LOWERCASE = "tokenizer.ggml.normalizer.lowercase"
|
||||
NORMALIZER_STRIP_ACCENTS = "tokenizer.ggml.normalizer.strip_accents"
|
||||
# FIM/Infill special tokens constants
|
||||
|
||||
@@ -1329,6 +1329,9 @@ class GGUFWriter:
|
||||
def add_suppress_tokens(self, tokens: Sequence[int]) -> None:
|
||||
self.add_array(Keys.Tokenizer.SUPPRESS_TOKENS, tokens)
|
||||
|
||||
def add_normalizer_nfc(self, value: bool) -> None:
|
||||
self.add_bool(Keys.Tokenizer.NORMALIZER_NFC, value)
|
||||
|
||||
def add_normalizer_lowercase(self, value: bool) -> None:
|
||||
self.add_bool(Keys.Tokenizer.NORMALIZER_LOWERCASE, value)
|
||||
|
||||
|
||||
@@ -1,10 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import array
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def out(line=""):
|
||||
print(line, end='\n') # noqa
|
||||
|
||||
|
||||
def normalization_tables():
|
||||
out(f"// Canonical normalization tables: Unicode {unicodedata.unidata_version}.")
|
||||
out("const std::unordered_map<uint32_t, uint8_t> unicode_combining_class = {")
|
||||
for cpt in range(0x110000):
|
||||
if value := unicodedata.combining(chr(cpt)):
|
||||
out(f"{{0x{cpt:06X}, {value}}},")
|
||||
out("};\n")
|
||||
out("const std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> unicode_decomposition = {")
|
||||
compositions = []
|
||||
for cpt in range(0x110000):
|
||||
if 0xAC00 <= cpt < 0xAC00 + 11172:
|
||||
continue
|
||||
raw = unicodedata.decomposition(chr(cpt))
|
||||
if not raw or raw.startswith("<"):
|
||||
continue
|
||||
parts = [int(part, 16) for part in raw.split()]
|
||||
assert 1 <= len(parts) <= 2
|
||||
first, second = parts[0], parts[1] if len(parts) == 2 else 0
|
||||
out(f"{{0x{cpt:06X}, {{0x{first:06X}, 0x{second:06X}}}}},")
|
||||
if second and unicodedata.normalize("NFC", chr(first) + chr(second)) == chr(cpt):
|
||||
compositions.append((first, second, cpt))
|
||||
out("};\n")
|
||||
out("const std::unordered_map<uint64_t, uint32_t> unicode_composition = {")
|
||||
for first, second, cpt in compositions:
|
||||
out(f"{{0x{((first << 32) | second):016X}ULL, 0x{cpt:06X}}},")
|
||||
out("};")
|
||||
|
||||
|
||||
# Generate only canonical tables, without downloading the UCD data.
|
||||
if sys.argv[1:] == ["--normalization-only"]:
|
||||
normalization_tables()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
MAX_CODEPOINTS = 0x110000
|
||||
|
||||
UNICODE_DATA_URL = "https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt"
|
||||
@@ -155,10 +195,6 @@ for codepoint, norm in table_nfd:
|
||||
# Generate 'unicode-data.cpp':
|
||||
# python ./scripts//gen-unicode-data.py > unicode-data.cpp
|
||||
|
||||
def out(line=""):
|
||||
print(line, end='\n') # noqa
|
||||
|
||||
|
||||
out("""\
|
||||
// generated with scripts/gen-unicode-data.py
|
||||
|
||||
@@ -194,3 +230,5 @@ out("const std::vector<range_nfd> unicode_ranges_nfd = { // start, last, nfd")
|
||||
for triple in ranges_nfd:
|
||||
out("{0x%06X, 0x%06X, 0x%06X}," % triple)
|
||||
out("};\n")
|
||||
|
||||
normalization_tables()
|
||||
@@ -421,6 +421,7 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
|
||||
{ LLM_KV_TOKENIZER_HF_JSON, "tokenizer.huggingface.json" },
|
||||
{ LLM_KV_TOKENIZER_RWKV, "tokenizer.rwkv.world" },
|
||||
{ LLM_KV_TOKENIZER_CHAT_TEMPLATE, "tokenizer.chat_template" },
|
||||
{ LLM_KV_TOKENIZER_NORMALIZER_NFC, "tokenizer.ggml.normalizer.nfc" },
|
||||
{ LLM_KV_TOKENIZER_NORMALIZER_LOWERCASE, "tokenizer.ggml.normalizer.lowercase" },
|
||||
{ LLM_KV_TOKENIZER_NORMALIZER_STRIP_ACCENTS, "tokenizer.ggml.normalizer.strip_accents" },
|
||||
{ LLM_KV_TOKENIZER_FIM_PRE_ID, "tokenizer.ggml.fim_pre_token_id" },
|
||||
|
||||
@@ -394,6 +394,7 @@ enum llm_kv {
|
||||
LLM_KV_TOKENIZER_HF_JSON,
|
||||
LLM_KV_TOKENIZER_RWKV,
|
||||
LLM_KV_TOKENIZER_CHAT_TEMPLATE,
|
||||
LLM_KV_TOKENIZER_NORMALIZER_NFC,
|
||||
LLM_KV_TOKENIZER_NORMALIZER_LOWERCASE,
|
||||
LLM_KV_TOKENIZER_NORMALIZER_STRIP_ACCENTS,
|
||||
LLM_KV_TOKENIZER_FIM_PRE_ID,
|
||||
|
||||
+17
-3
@@ -1021,7 +1021,20 @@ struct llm_tokenizer_ugm_session {
|
||||
|
||||
// normalize the input first
|
||||
std::string normalized;
|
||||
normalize(text, &normalized);
|
||||
if (vocab.get_normalizer_opts().nfc) {
|
||||
try {
|
||||
std::string canonical;
|
||||
for (const uint32_t cpt : unicode_cpts_normalize_nfc(unicode_cpts_from_utf8(text))) {
|
||||
canonical += unicode_cpt_to_utf8(cpt);
|
||||
}
|
||||
normalize(canonical, &normalized);
|
||||
} catch (const std::invalid_argument &) {
|
||||
// Use the existing UGM normalization path for malformed input.
|
||||
normalize(text, &normalized);
|
||||
}
|
||||
} else {
|
||||
normalize(text, &normalized);
|
||||
}
|
||||
size_t input_len = normalized.size();
|
||||
if (input_len == 0) {
|
||||
return;
|
||||
@@ -1950,7 +1963,7 @@ struct llama_vocab::impl {
|
||||
bool escape_whitespaces = true;
|
||||
bool treat_whitespace_as_suffix = false;
|
||||
|
||||
// BertNormalizer options
|
||||
// Tokenizer normalizer options
|
||||
llama_vocab::normalizer_options normalizer_opts;
|
||||
|
||||
std::unordered_map<std::string, llama_token> token_to_id;
|
||||
@@ -2778,7 +2791,8 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
}
|
||||
}
|
||||
|
||||
// BertNormalizer options
|
||||
// Tokenizer normalizer options
|
||||
ml.get_key(LLM_KV_TOKENIZER_NORMALIZER_NFC, normalizer_opts.nfc, false);
|
||||
ml.get_key(LLM_KV_TOKENIZER_NORMALIZER_LOWERCASE, normalizer_opts.lowercase, false);
|
||||
normalizer_opts.strip_accents = normalizer_opts.lowercase;
|
||||
ml.get_key(LLM_KV_TOKENIZER_NORMALIZER_STRIP_ACCENTS, normalizer_opts.strip_accents, false);
|
||||
|
||||
@@ -83,6 +83,7 @@ struct llama_vocab {
|
||||
};
|
||||
|
||||
struct normalizer_options {
|
||||
bool nfc = false;
|
||||
bool lowercase = true;
|
||||
bool strip_accents = true;
|
||||
// TODO: clean_text, handle_chinese_chars
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
@@ -11,6 +12,10 @@ struct range_nfd {
|
||||
uint32_t nfd;
|
||||
};
|
||||
|
||||
extern const std::unordered_map<uint32_t, uint8_t> unicode_combining_class;
|
||||
extern const std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> unicode_decomposition;
|
||||
extern const std::unordered_map<uint64_t, uint32_t> unicode_composition;
|
||||
|
||||
static const uint32_t MAX_CODEPOINTS = 0x110000;
|
||||
|
||||
extern const std::initializer_list<std::pair<uint32_t, uint16_t>> unicode_ranges_flags;
|
||||
|
||||
@@ -1279,6 +1279,92 @@ std::vector<uint32_t> unicode_cpts_normalize_nfd(const std::vector<uint32_t> & c
|
||||
return result;
|
||||
}
|
||||
|
||||
static uint8_t unicode_canonical_class(uint32_t cpt) {
|
||||
const auto it = unicode_combining_class.find(cpt);
|
||||
return it == unicode_combining_class.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
static void unicode_canonical_decompose(uint32_t cpt, std::vector<uint32_t> & result, std::vector<uint32_t> & pending) {
|
||||
pending.push_back(cpt);
|
||||
while (!pending.empty()) {
|
||||
cpt = pending.back();
|
||||
pending.pop_back();
|
||||
// Hangul syllables use algorithmic decomposition (UAX #15).
|
||||
if (cpt >= 0xAC00 && cpt < 0xAC00 + 11172) {
|
||||
const uint32_t index = cpt - 0xAC00;
|
||||
result.push_back(0x1100 + index / 588);
|
||||
result.push_back(0x1161 + (index % 588) / 28);
|
||||
if (index % 28) {
|
||||
result.push_back(0x11A7 + index % 28);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const auto it = unicode_decomposition.find(cpt);
|
||||
if (it == unicode_decomposition.end()) {
|
||||
result.push_back(cpt);
|
||||
continue;
|
||||
}
|
||||
if (it->second.second) {
|
||||
pending.push_back(it->second.second);
|
||||
}
|
||||
pending.push_back(it->second.first);
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t unicode_canonical_compose(uint32_t first, uint32_t second) {
|
||||
if (first >= 0x1100 && first < 0x1100 + 19 && second >= 0x1161 && second < 0x1161 + 21) {
|
||||
return 0xAC00 + ((first - 0x1100) * 21 + second - 0x1161) * 28;
|
||||
}
|
||||
if (first >= 0xAC00 && first < 0xAC00 + 11172 && (first - 0xAC00) % 28 == 0 && second > 0x11A7 && second < 0x11A7 + 28) {
|
||||
return first + second - 0x11A7;
|
||||
}
|
||||
const auto it = unicode_composition.find((uint64_t(first) << 32) | second);
|
||||
return it == unicode_composition.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> unicode_cpts_normalize_nfc(const std::vector<uint32_t> & cpts) {
|
||||
std::vector<uint32_t> decomposed;
|
||||
decomposed.reserve(cpts.size());
|
||||
std::vector<uint32_t> pending;
|
||||
for (const uint32_t cpt : cpts) {
|
||||
unicode_canonical_decompose(cpt, decomposed, pending);
|
||||
}
|
||||
|
||||
// Sort each run of combining marks; equal classes retain their order.
|
||||
for (size_t i = 0; i < decomposed.size();) {
|
||||
if (unicode_canonical_class(decomposed[i]) == 0) {
|
||||
++i;
|
||||
}
|
||||
const size_t begin = i;
|
||||
while (i < decomposed.size() && unicode_canonical_class(decomposed[i]) != 0) {
|
||||
++i;
|
||||
}
|
||||
std::stable_sort(decomposed.begin() + begin, decomposed.begin() + i, [](uint32_t a, uint32_t b) {
|
||||
return unicode_canonical_class(a) < unicode_canonical_class(b);
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<uint32_t> result;
|
||||
result.reserve(decomposed.size());
|
||||
size_t starter = 0;
|
||||
uint8_t previous_class = 0;
|
||||
for (const uint32_t cpt : decomposed) {
|
||||
const uint8_t current_class = unicode_canonical_class(cpt);
|
||||
const uint32_t composite = !result.empty() && (previous_class == 0 || previous_class < current_class)
|
||||
? unicode_canonical_compose(result[starter], cpt) : 0;
|
||||
if (composite) {
|
||||
result[starter] = composite;
|
||||
} else {
|
||||
if (current_class == 0) {
|
||||
starter = result.size();
|
||||
}
|
||||
result.push_back(cpt);
|
||||
previous_class = current_class;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> unicode_cpts_from_utf8(const std::string & utf8) {
|
||||
std::vector<uint32_t> result;
|
||||
result.reserve(utf8.size());
|
||||
|
||||
@@ -97,6 +97,7 @@ uint32_t unicode_cpt_from_utf8(const std::string & utf8, size_t & offset);
|
||||
std::vector<uint32_t> unicode_cpts_from_utf8(const std::string & utf8);
|
||||
|
||||
std::vector<uint32_t> unicode_cpts_normalize_nfd(const std::vector<uint32_t> & cpts);
|
||||
std::vector<uint32_t> unicode_cpts_normalize_nfc(const std::vector<uint32_t> & cpts);
|
||||
|
||||
unicode_cpt_flags unicode_cpt_flags_from_cpt (uint32_t cpt);
|
||||
unicode_cpt_flags unicode_cpt_flags_from_utf8(const std::string & utf8);
|
||||
|
||||
@@ -2,9 +2,31 @@
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
int main() {
|
||||
const std::vector<std::pair<std::vector<uint32_t>, std::vector<uint32_t>>> normalization = {
|
||||
{{}, {}},
|
||||
{{0x65, 0x301}, {0xE9}},
|
||||
{{0xE9}, {0xE9}},
|
||||
{{0x212B}, {0xC5}},
|
||||
{{0x61, 0x315, 0x300}, {0xE0, 0x315}},
|
||||
{{0x61, 0x305, 0x301}, {0x61, 0x305, 0x301}},
|
||||
{{0x301, 0x327}, {0x327, 0x301}},
|
||||
{{0x1100, 0x1161, 0x11A8}, {0xAC01}},
|
||||
{{0xAC01}, {0xAC01}},
|
||||
{{0xFB01, 0xFF21}, {0xFB01, 0xFF21}},
|
||||
{{0x958}, {0x915, 0x93C}},
|
||||
{{0x1D15E}, {0x1D157, 0x1D165}},
|
||||
};
|
||||
for (const auto & test : normalization) {
|
||||
if (unicode_cpts_normalize_nfc(test.first) != test.second || unicode_cpts_normalize_nfc(test.second) != test.second) {
|
||||
fprintf(stderr, "unexpected NFC normalization\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::string> regex_exprs = {
|
||||
"[~][A-Za-z]+| ?[\\p{S}]+|\\s+",
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user