tokenizers v1: кодиране, декодиране и мащабиране, измерени
С ускоряването на моделите и мащабирането на работните натоварвания този баланс започва да се променя. Обучението върху огромни набори от данни, обслужването на множество едновременни заявки или многократната обработка на дълги входове могат да натоварят токенизатора дотолкова, че той да остави модела без данни.
Ето защо решихме да се съсредоточим сериозно върху производителността на предстоящата версия 1 на tokenizers. Токенизацията трябва да е лека и да се мащабира заедно с работния ви процес. Графичните ви процесори никога не трябва да бездействат, докато чакат централният процесор да завърши токенизацията.
В тази статия разглеждаме какво прави v1 по-бърза от v0.23, често с десетки пъти.
Тази работа изцяло стана възможна благодарение на останалата част от екосистемата. Токенизацията е много активна област на работата с отворен код и библиотеки като gigatoken, tiktoken, kitoken, tokie, fastokens, wordchipper и ai-tokenizer, както и много други, всяка поотделно разшириха представите за това какво може да бъде един бърз токенизатор. Прочетохме тази работа и няколко от идеите по-долу достигнаха до нас, защото друг проект показа, че си струва да бъдат изпробвани.
Преди този рефакторинг tokenizers беше далеч от възможната си производителност, така че приносът към него може да не е изглеждал оправдан. С този рефакторинг се надяваме да покажем ясно, че възнамеряваме tokenizers да бъде библиотека, към която си струва да се допринася.
Благодарим също на IBM, NVIDIA и екипа на ExecuTorch за приноса на корекции и за помощта при тестването върху широк набор от хардуерни конфигурации, с което разширихме поддръжката на платформи.
Резултати
Представяме резултати за кандидата за издание на tokenizers v1 в сравнение с други широко използвани алтернативи. Разглеждаме еднопоточна и многопоточна работа, мащабиране между нишки, сравнение по модели, сравнение по езици, латентност, пропускателна способност при декодиране, heap памет, както и размера на crate.
Провеждаме тестовете от хранилището tokbench и добавяме команда за повторно изпълнение на бенчмарковете върху вашия хардуер, ако желаете.
Какво представлява V1
v1 ще създава същите идентификатори на токени като v0.23. Целта беше да запазим изхода, API интерфейса, речника и ранговете на сливане и да подобрим всичко, което може да бъде подобрено. Това включва и обхвата. Библиотеката остава универсална за различните семейства токенизатори, вместо да се специализира върху BPE, така че v1 зарежда всичко, което зареждаше v0.23.
Токенизаторът преобразува текста в списък от цели числа, които моделът прочита. tokenizers извършва това преобразуване на четири етапа. Нормализацията прилага операции като преобразуване в малки букви или нормализация на Unicode към необработения текст. Предтокенизацията разделя текста на по-малки части, наречени предтокени. Моделът превръща всеки предтокен в токени и ги съпоставя с идентификатори в речника си. Постобработката добавя всички специални токени, които моделът очаква.
Етапът на модела е мястото, където се извършва по-голямата част от описаната тук работа. Осем от десетте измерени в тази статия семейства модели използват кодиране чрез двойки байтове, или BPE. BPE започва от байтовете на даден предтокен и многократно обединява съседната двойка с най-висок ранг, докато не остане двойка с ранг. Рангът се научава при обучението на токенизатора и се доставя заедно с него, така че един и същ текст винаги създава едни и същи идентификатори. Сливането никога не преминава границата на предтокен. Другите две семейства използват WordPiece и Unigram — двата други типа модели, които библиотеката поддържа.
Страницата за конвейера за токенизация документира четирите етапа. Страницата за алгоритмите за токенизация документира BPE, WordPiece и Unigram.
Работихме по всеки етап. Ето промените, които имаха значение:
| промяна | какво прави |
|---|---|
| разделяне на workspace | един crate се превърна в workspace: tk-encode е задължителният runtime, а tk-serialize, tk-convert и tk-train се свързват само когато приложението има нужда от тях |
| модел без алокации | работният набор за сливане се съхранява във временен буфер, притежаван от извикващия; цикълът никога не докосва алокатора |
| bitcannon | шаблонът за разделяне се превръща в булеви операции върху битови потоци, като се използват SIMD инструкции за откриване на разделянията вместо regex engine |
| преработен цикъл на сливане | частите, които се сливат, образуват интрузивен двусвързан списък в един предварително заделен буфер, така че сливането актуализира два индекса вместо да премества данни |
| кеш на думи | локален за нишката кеш, който съпоставя байтовете на предтокена с готовите идентификатори, така че повтарящата се дума се слива само веднъж |
| нативен паралелизъм | един споделен токенизатор кодира едновременно от множество нишки; всяка нишка получава временния си буфер и кеша на думите от собствен поднабор, така че нишките вече не чакат на една обща блокировка (#2365) |
Разделянето: битови потоци вместо regex
BPE моделите използват регулярен израз, за да разделят входния текст на по-малки, по-лесни за обработка части, наречени предтокени. Сливането се извършва вътре в предтокена и никога не преминава границата между два от тях, така че това разделяне определя какво ще види останалата част от конвейера.
Този регулярен израз е фиксиран параметър на модела. Той се доставя заедно с токенизатора и никога не се променя по време на изпълнение, така че няма нужда универсален regex engine да го интерпретира при всяко кодиране. Еквивалентна функция за разделяне може да бъде написана ръчно веднъж за шаблона, който конкретният модел действително използва.
След това ръчно написаната функция може да използва SIMD инструкциите (една инструкция, множество данни) на съвременния процесор, които прилагат една операция върху много байтове едновременно и са подходящи за UTF-8 текст. bitcannon разглежда байтовете на входа като паралелни потоци от битове, така че границите се получават чрез булеви операции върху цели регистри, вместо чрез сканиране, което се придвижва знак по знак. Той обработва 64 байта за една регистърна операция. Същата идея стои в основата на Parabix за обработка на текст и на simdjson за JSON.
Това зависи от разпознаването на шаблона. Няколко граматики обхващат повечето BPE модели на байтово ниво, а токенизатор, чийто шаблон не е сред тях, запазва regex пътя и не получава никакво от това ускорение. Ето защо подобренията по-горе се различават толкова много.
Кешът на думите
Реалният текст съдържа много повтарящи се думи. Тъй като BPE винаги създава едни и същи идентификатори на токени за даден предтокен, v1 може да запази резултата след първата му обработка. Локален за нишката кеш съпоставя байтовете на всеки предтокен с идентификаторите му на токени, което позволява на следващите срещания да пропуснат процеса на сливане.
Естествено, с нарастването на входа броят на уникалните думи може да расте по-бавно от общия брой думи. Тогава повтарящите се думи представляват все по-голям дял от входа. Все пак се появяват нови думи, което обяснява случайните пропуски в анимацията по-долу.
Възпроизведете резултата за споделения префикс с:
tokbench measure prefix-sharing \
--engine pipeline \
--engine hf-tokenizers \
--compare-to pipeline-no-cache \
--corpus agentic_swe
Кеширането работи най-добре, когато входът съдържа повтарящи се предтокени. Вход с малко повтарящи се предтокени може да плати цената на търсенията, без да получи много попадения.
Цикълът на сливане
Следващият основен разход идва от цикъла за сливане на BPE. За всеки предтокен цикълът многократно намира съседната двойка с най-висок приоритет и я слива. Предишната реализация заделяше нова памет при всяко извикване и изграждаше нова приоритетна опашка за всеки предтокен.
v1 използва повторно временен буфер, притежаван от извикващия, като премахва тези повтарящи се алокации. Тя съхранява символите в плосък масив и свързва съседните символи чрез позициите им в този масив, което прави актуализациите по време на сливане по-евтини. Освен това обработва група предтокени в едно извикване на модела.
Всяка кандидат-двойка също се пакетира в една 64-битова стойност, като рангът на сливането е във високите битове. Така сравняването на два кандидата се свежда до сравняване на две цели числа, а „няма сливане тук“ е най-голямата възможна стойност, така че цикълът намира следващото си сливане без условен преход.
Методика
Малките разлики в дизайна на бенчмарка могат да доведат до големи разлики в производителността на токенизатора. Използвахме следните правила, за да запазим сравнението последователно между различните engine-и.
| правило | защо |
|---|---|
| един цикъл за измерване на времето | всеки engine изпълнява идентичен цикъл; няма оптимизиран път за отделен engine |
| зареждането е изключено | зареждането на речника се измерва отделно, никога вътре в encode |
| проверка чрез hash на идентификаторите | FNV-1a върху изходните идентификатори трябва да съвпада точно с базовата стойност |
| само общи клетки | медианите се изчисляват върху клетки, които всеки engine е изпълнил и проверил |
| пълно сканиране за всеки процес | всяко повторение започва в нов процес и запазва всяка клетка |
| фиксиране към физически ядра | работниците се фиксират към осем различни физически ядра, никога към съседни SMT нишки |
| независими Jobs | отделни Jobs измерват вариациите между хостовете |
Многократното кодиране на един документ може да бъде по-бързо от кодирането на поток от различни документи в една и съща компилация. Първият подход измерва производителността, когато целият документ вече е представен в кеша. Вторият измерва производителността върху нов вход, като позволява на вече видените предтокени да останат кеширани.
И двете условия понякога се описват като „топли“, въпреки че измерват различни работни натоварвания. Нашите основни резултати използват различни документи, а целият корпус е твърде голям, за да се побере в кеша. Бенчмарковете на токенизаторите трябва да посочват кое работно натоварване използват, защото изборът може да доминира над резултата.
Какво означава всичко това
При десетте семейства модели, които v1 покрива чрез пътя си за кодиране, тя кодира текст от 3 до 30 пъти по-бързо от v0.23 с една нишка на Apple M4 Max. Долният край е t5-base, а горният — gpt2. Тя се мащабира до 76% от линейното мащабиране при осем работници. През всички тези промени v1 създава точно същите идентификатори на токени като издадената библиотека.
Общото подобрение идва от съвместната работа на няколко промени: ръчно написан разделител вместо regex engine, кеш, който отговаря за повтаряща се дума, без да я слива отново, цикъл за сливане, който никога не докосва алокатора, и едно извикване на модела за група предтокени вместо по едно за всеки предтокен. Всяка от тях намалява извършваната работа в различна точка на конвейера.
Следващият приоритет е поддръжката на повече семейства модели. Преди 1.0.0 ще прехвърлим допълнителни модели към новия цикъл за сливане. След стабилизирането на кандидатите за издание следващата стъпка ще бъде пренасянето на подобренията в библиотеката transformers и в останалата част от екосистемата, която зависи от библиотеката tokenizers.
Тази публикация се генерира от резултатите на tokbench и ще бъде актуализирана с разширяването на поддръжката.
Как да го получите
Кандидат за издание на v1 е наличен в crates.io. API интерфейсът, който извиквате, е същият, който вече използвате, така че единственото, което се променя, е коя компилация инсталирате.
Обикновената инсталация е:
cargo add tokenizers --pre
Обучението е зад функция, включена по подразбиране, която добавя към зависимостите и C++ зависимост. Ако се нуждаете само от кодиране, изключете я, за да премахнете реализацията на обучението:
cargo add tokenizers --pre --no-default-features --features http
Кодирането не е променено: същото извикване, същите идентификатори.
use tokenizers::tokenizer::{Result, Tokenizer};
fn main() -> Result<()> {
let tokenizer = Tokenizer::from_pretrained("deepseek-ai/DeepSeek-V4-Flash", None)?;
let encoding = tokenizer.encode("Токенизаторът вече не е тясното място.", false)?; println!("{:?}", encoding.get_ids());
Ok(()) } ```
За група `encode_batch` е това, което се мащабира между ядрата. Именно това извикване измерва изгледът за мащабиране по-горе.
```rust
let encodings = tokenizer.encode_batch(documents, false)?;
Всяка стойност в тази публикация е измерена спрямо този crate. Python обвивките използват същия код и се изграждат от bindings/python, но добавят допълнителни разходи при всяко извикване, които не са включени в нито едно от тези измервания.
Напредък към V1
Бенчмарковете в тази публикация обхващат завършената работа по кандидата за издание, изброена първа. Останалите раздели показват какво все още е необходимо за 1.0.0 и какво планираме да проучим след това.
Кандидат за издание: реализиран
Тази работа е налична в предварителното Rust издание в crates.io:
cargo add tokenizers --pre
- разделяне на workspace: разделяне на единичния crate на
tk-encode,tk-serialize,tk-convertиtk-train, така че приложението да свързва само това, което използва - bitcannon: замяна на regex разделянето в пътя за кодиране с операции върху битови потоци, покриващи GPT-2, cl100k, o200k, Tekken и DeepSeek. Това замени машините с крайни състояния, които бяха доставени първоначално #2201 #2317
- WordCache: повторно използване на идентификаторите на токени от вече обработени предтокени #2262,
af5a3e3 - по-бързи структури за търсене и сливане: добавяне на FlatCache, MPHF RankStore, инкрементално сливане и BucketVocabStore #2190 #2188
- повторно използваема памет на модела: преместване на временното състояние на модела във временни буфери, така че токенизацията да не заделя памет при всяко извикване #2175 #2183
- постобработка на конвейера: предоставяне на постобработката като етап на конвейера
STAGE_POST#2182 - пакетни извиквания на модела: обработка на множество диапазони от предтокени в едно извикване #2304
- по-бързо декодиране: директно записване на декодираните байтове в повторно използваем буфер, избягване на междинни низове и копирания, ускоряване на търсенето на токени, поддръжка на буферирано поточно предаване и паралелно декодиране на групи
- поддръжка на
role_to_token#2343 - Node.js обвивки #2281
1.0.0
- една реализация за кодиране: използване на
tk-encodeпо време на валидирането на обучението, така че обучението и извеждането да не могат да създадат различни резултати от токенизацията - незадължителни отмествания и маски: изчисляване на тези метаданни само когато са заявени, като те се запазват извън пътя, който използва само идентификатори на токени
- преработване на нормализаторите
- поддръжка на bitnorm с изграждане върху atomnorm #2209
- предварително компилиран spm
- по-прости Python обвивки: намаляване на блокировките, wrapper типовете и ръчно написания dispatch код, като същевременно се запазят наследяването, сериализацията, персонализираните декодери, поведението при мутация и поддръжката на CPython със свободни нишки
- C и C++ обвивки само за извеждане за ExecuTorch и llama.cpp, с възможна последваща поддръжка на JVM, Swift и Go
След 1.0.0
- tok-devices: проучване на GPU кодиране и пакетно декодиране, като текстът и идентификаторите на токени остават на устройството. Декодерът ще качи речника веднъж, ще изчисли изходните позиции паралелно и ще събере съответните байтове на GPU. Това ще бъде незадължителен компонент, предназначен за големи групи, който подлежи на допълнително прототипиране и измерване.
Общност
· Регистрирайте се или влезте, за да коментирате
Преведено автоматично от английски. Оригиналната статия е на връзката по-долу.
