Технологии
«Можно просто пропускать нормализацию на участках ASCII»: Hugging Face ускорила токенизацию BERT примерно в 50 раз относительно 0.23
15 октября 2026 года Hugging Face объяснила, как ускорила нормализацию текста в токенизаторе BERT примерно в 50 раз относительно версии 0.23 — то есть ещё в 5,7 раза сверх того, что уже дала ветка v1.0. Причина нашлась в профиле: 88% времени кодирования уходило на нормализацию, а около двух третей всего вызова — на бинарный поиск по таблице из 1 567 символов в проверке «этот символ диакритический?». Ускорение построено на трёх наблюдениях о реальном тексте: почти весь он ASCII, остальное кластеризовано по письменностям, а уже нормализованный текст трогать не нужно. На момент публикации изменения находятся в статусе черновика pull request.

Ветка v1 ускорила BPE, но обошла BERT
Hugging Face последние месяцы возвращает к жизни библиотеку tokenizers: цель — быстрее кодирование и меньший размер, а улучшения планируется зафиксировать в релизе v1.0.0. Основным ориентиром было кодирование в мегабайтах в секунду для моделей на BPE, и классические BPE-токенизаторы там ускорились до 30 раз относительно 0.23.
Но bert-base-uncased в той работе ускорился только примерно в 8,6 раза, и автора это не устроило. Новая публикация объясняет, как этот же путь стал быстрее ещё в 5,7 раза — суммарно около 50 раз относительно 0.23.
Существенная оговорка: на момент написания статьи изменения ещё не влиты — они находятся в статусе черновика pull request в репозитории huggingface/tokenizers.
Почему BERT всё ещё считается
BERT вышел в 2018 году, до ChatGPT, но его нормализатор продолжает работать на масштабе, которого большинство языковых моделей не достигает: BERT-подобные модели стоят за эмбеддингами, поиском и RAG, реранкерами и классификаторами.
Три самые скачиваемые модели на хабе используют именно нормализатор BERT: all-MiniLM-L6-v2 (218 млн скачиваний в месяц), кросс-энкодер ms-marco-MiniLM-L6-v2 (79 млн) и bge-small-en-v1.5 (59 млн); сам bert-base-uncased идёт шестым с 34 млн.
В сумме по 10 000 самым скачиваемым моделям хаба токенизаторы на BPE дают 842 млн скачиваний в месяц, а WordPiece — 627 млн, и каждое из них проходит через нормализатор BERT. Нормализация при этом не только про BERT: больше половины скачиваний BPE проходят через одну из форм Unicode, чаще всего NFC, как у Qwen или ModernBERT.
88% времени кодирования — на нормализацию
Профиль тёплого прогона на английском тексте с bert-base-uncased показал, где на самом деле уходит время: 88% занимает нормализация, а WordPiece — та часть, которая выбирает токены, — только 8%. Около двух третей всего кодирования приходится на неочевидный binary_search_by из проверки char::is_mark_nonspacing() в крейте unicode_categories, то есть на ответ на вопрос, является ли символ диакритическим знаком.
Под этой проверкой лежит бинарный поиск по отсортированному массиву из 1 567 символов размером 6 КБ: примерно 11 сравнений на каждый запрос, и каждое сравнение — ветвление, которое процессор не может предсказать. Такой запрос выполнялся на каждом символе текста, включая «s», «l», «o» и «w».
Пять шагов нормализатора и вся сложность Unicode
BertNormalizer с lowercase=True — вариант из bert-base-uncased — делает пять вещей в строгом порядке: чистит текст от управляющих символов и приводит любые пробельные символы к обычному пробелу 0x20; ставит пробелы с каждой стороны от иероглифов CJK; разбирает символы по форме NFD; убирает диакритические знаки, которые NFD отделил; приводит текст к нижнему регистру.
Сложность здесь родом из Unicode: кодовые точки занимают диапазон от 0 до 0x10FFFF (около 1,1 млн возможных значений), из которых реально задействовано 159 801. Один и тот же символ на экране может быть записан по-разному — «é» бывает одной кодовой точкой U+00E9 или двумя, «e» плюс U+0301, — поэтому Unicode определяет четыре формы нормализации: NFC и NFD составляют или разбирают символы, а формы NFKC и NFKD дополнительно сворачивают совместимые символы вроде лигатур и цифр в кружках.
Три наблюдения о реальном тексте
Первое: почти весь текст — ASCII, а ASCII устойчив к нормализации. Ни одна форма нормализации не меняет ASCII-символ, и диакритический знак не может присоединиться через него, поэтому такие участки можно не нормализовать вовсе. В UTF-8 байт является ASCII тогда и только тогда, когда у него нулевой старший бит, а регистр в ASCII отличается единственным битом 0x20 — то есть приведение к нижнему регистру превращается в одно побитовое ИЛИ.
Для быстрых пропусков используются векторные инструкции: нормализатор загружает 16 байт, одной инструкцией собирает их старшие биты в целое число и проверяет его — если результат ноль, весь блок пропускается и никогда не декодируется, не ищется в таблицах и не копируется; если нет, ещё одна инструкция находит позицию первого не-ASCII байта. Это примерно три инструкции на 16 байт там, где прежний код разбирал и искал каждый символ по одному.
Второе: остальной текст кластеризован. Письменность занимает непрерывный участок кодовых точек — кириллица, например, это 256 идущих подряд значений U+0400–U+04FF, — а сам текст использует очень мало письменностей одновременно. Поэтому ответ на вопрос, может ли нормализация тронуть символ, вычисляется один раз для страницы из 256 кодовых точек и запоминается: дальше любой символ с этой страницы стоит одного чтения памяти вместо бинарного поиска. Страница — это четыре 64-байтовые кэш-линии, которые остаются в самом быстром кэше рядом с ядром.
Третье: текст чаще всего уже нормализован. Символы, которые формально могут измениться, обычно не меняются, поэтому применяется «ленивая» стратегия: участки, где делать нечего, не копируются вовсе, а память выделяется один раз — если ничего не изменилось, входная строка возвращается как есть, без выделения; при первом реальном изменении результат выделяется один раз, и неизменённые части копируются крупными блоками.
Что показали замеры
Доля нормализации в кодировании упала с 88% до 13%. На графике пламени, где ширина блока равна времени кодирования, тот же английский текст дал 23 МБ/с на основной ветке против 151 МБ/с на оптимизированной.
Сквозное ускорение зависит от доли ASCII в тексте: сильнее всего выигрывают английский и исходный код, а русский, греческий и арабский, где ASCII только около 11%, всё равно получают ускорение в 2,2–3,2 раза — уже за счёт кэша страниц. Китайский текст не ускоряется вовсе.
Для ModernBERT, который использует стандартную форму NFC, картина другая: на английском ускорения почти нет (1,06 раза), потому что основная ветка и так пропускала ASCII, зато код, арабский и русский дают около двукратного роста.
Отдельно проверено, что выход не изменился: каждый прогон считает хеш от полученных идентификаторов токенов, и хеши совпадают между основной веткой, оптимизированной веткой и версией 0.23.2 на всех корпусах.
Где стало хуже
Автор прямо перечисляет случаи, где оптимизация проигрывает: добавленный механизм приходится оплачивать очень маленьким входам, а письменности вроде CJK почти не ускоряются — в них мало ASCII, и в BERT каждый китайский иероглиф всё равно требует пробелов с двух сторон, то есть они платят за оптимизации, которыми не пользуются.
На замерах только нормализатора медленнее уровня шума оказались 55 ячеек из 320, из них 19 — на слайсах не длиннее 32 байт. Очень короткие входы могут замедлиться до 0,35 раза, например StripAccents на 8 байтах китайского текста, а cased-вариант BERT на китайском медленнее на 8–15% начиная с 64 байт.
Как это измерено и что доступно сейчас
Замеры сделаны на трёх машинах: двух серверах с AMD EPYC 7R13 в Hugging Face Jobs и одном внутреннем на Intel Xeon Platinum 8488C. Перед тем как доверять числам, тот же бинарник прогнали сам против себя: разброс между раундами составил 0,35% на EPYC и около 2% на Xeon.
Сравнивались основная ветка библиотеки tokenizers, отставшая на несколько коммитов от 1.0.0-rc2 (коммит fb49a29), и оптимизированная ветка из pull request 2508 (5b5a3fc); обе собраны из исходников одним инструментарием — rustc 1.99.0, lto = «fat», codegen-units = 1, — а версия 0.23.2 взята из crates.io. Каждое измерение — это однопоточное кодирование корпуса на 5–6 МБ в цикле в течение пяти секунд: один разминочный раунд, затем пять раундов, в которых основная ветка и оптимизированная чередуются, а в отчёт идёт медиана отношений по раундам.
Корпуса — англоязычная, русская, греческая, арабская и китайская проза плюс смешанный исходный код из набора hf-internal-testing/tokenizers-test-data на ревизии 5f5c652. Профили сняты pprof-rs с частотой 999 Гц внутри процесса (perf в контейнерах заданий не запускался), а счётчики инструкций подтверждены callgrind: на 1 МБ английского текста кодирование падает с 482 млн инструкций до 72 млн, то есть в 6,7 раза, а доля нормализатора — с 87% до 12%.
Для воспроизведения опубликован отдельный репозиторий с кодом замеров, а в браузере доступно интерактивное демо: оно запускает настоящий код на Rust, скомпилированный в WebAssembly, и показывает пошагово, что происходит с введённым текстом.
Что здесь проверяемо, а что — заявление
Числа в публикации — собственные замеры автора, а не независимое воспроизведение: сравниваются две ветки одного репозитория на одних и тех же корпусах, воспроизводимость обеспечена опубликованным кодом, но сторонних прогонов на момент публикации нет.
Работа подана как продолжение линии v1: предыдущая публикация Hugging Face по tokenizers уже отмечала, что выигрыш на WordPiece меньше, чем на BPE, и что именно на этих семействах авторы сосредоточатся дальше — новый материал закрывает как раз эту часть.
Формально ускорения пока нет в стабильной библиотеке: pull request 2508 в репозитории huggingface/tokenizers на момент проверки имеет статус черновика (draft) и не влит.
Материал опубликован 15 октября 2026 года в блоге Hugging Face от имени Саймона Брандейса (Simon Brandeis), участника организации huggingface, и платформа помечает его ярлыком Community Article.
Первоисточники
- Hugging Face — Simon Brandeis, «Tokenmaxxing BERT: 50x faster tokenization» (15 октября 2026)
- GitHub — huggingface/tokenizers, pull request 2508 «perf(normalizers): Improve unicode and BERT normalizers» (статус черновика на 15 октября 2026)
- GitHub — SBrandeis/tokenmaxxing-bert-reproducer: код воспроизведения замеров (15 октября 2026)
- Hugging Face Spaces — интерактивное демо нормализатора BERT: настоящий код на Rust, скомпилированный в WebAssembly (15 октября 2026)
- Hugging Face — Arthur Zucker, Simon Brandeis, Luc Georges, Lysandre, «tokenizers v1: encode, decode and scaling, measured» (21 сентября 2026)