Считаем количество токенов для LLM в исходниках ядра Linux и не только…

—

от автора

Эта статья про новое расширение ахритектуры трансформеров – Titan от Google –, позволяющее расширить рамки LLM до 2 млн токенов, побудила поинтересоваться, сколько токенов, пригодных для LLM, содержат исходники колоссального софта.

Какой открытый софт будем „препарировать“:

  • MySQL

  • VS Code

  • Blender

  • Linux*

  • LLVM*

Итого 5 крупных и известных проектов. Подсчёт происходил на актуальных версиях исходников. Звёздочками отмечен тот софт, кодовая база которого весит больше одного ГБ.

Как будем считать

Сначала скачиваем репозиторий с исходниками, желательно удаляем папку .git или .hg (Firefox использует Mercurial вместо Git), если она есть. Далее перегоняем все исходники в один текстовый файл. Подобным образом кодовую базу обрабатывает сервис GitIngest (их GitHub). Но там есть ограничение на время работы в 20 секунд, чего, коенчно, не хватает для перегонки почти 1,5 ГБ исходников того же ядра, да и написан он на Python. Поэтому для решения этой проблемы необходимо проводить подготовку кодовой базы на своём компьютере с использованием более высокопроизводительного способа. Таким способом стала небольшая многопоточная программа на C++, которую написала китайская LLM DeepSeek — аналог ChatGPT. По завершении работы программы получается текстовый файл prompt.txt со следующей структурой:

  • Дерево кодовой базы, так как структура кодовой базы является не менее важной информацией, чем её содержимое

  • Содержимое всех файлов в таком markdown-подобном формате, где начало и конец содержимого файлов обозначается тремя грависами `:

    <file_name.file_extension>```\n<file_content>\n```\n\n

Также эта программа выводит число «слов» в кодовой базе — простой подсчёт по разделению кодовой базы по пробелам и переносам строк, это число намного меньше, чем число токенов, подсчитанное продвинутым токенизатором от OpenAI, о котором далее. Эту часть программы следовало бы убрать и сделать это достаточно легко, но можно просто прерывать процесс выполенения в терминале.

Исходники:

Дисклеймер

Автор не умеет писать на C++, весь код сгенерирован LLM и толком не прочитан, только проверена его работоспособность. Хоть в коде и игнорируется папка гита, но тем не менее файлы из неё попадают в итоговый файл, поэтому я и написал, что желательно её удалить, если она есть. Хотя в случае скачивания исходников с GitHub в zip-архиве (кнопка code → download zip) её нет.

Код на C++, который я бегло просмотрел и почти не читал
#include <filesystem> #include <fstream> #include <string> #include <vector> #include <iostream> #include <cstddef> #include <algorithm> #include <cctype> #include <thread> #include <mutex> #include <sstream> #include <atomic> #include <memory>  namespace fs = std::filesystem;  // Генерация дерева директорий std::string generate_tree(const fs::path& path, const std::string& prefix = "") {     std::string tree;     std::vector<fs::path> entries;     for (const auto& entry : fs::directory_iterator(path)) {         if (entry.path().filename() == ".git") continue;         entries.push_back(entry.path());     }     std::sort(entries.begin(), entries.end(), [](const fs::path& a, const fs::path& b) {         return a.filename() < b.filename();     });     for (size_t i = 0; i < entries.size(); ++i) {         bool is_last = (i == entries.size() - 1);         std::string connector = is_last ? "└── " : "├── ";         if (fs::is_directory(entries[i])) {             tree += prefix + connector + entries[i].filename().string() + "/\n";         } else {             tree += prefix + connector + entries[i].filename().string() + "\n";         }         if (fs::is_directory(entries[i])) {             std::string new_prefix = prefix + (is_last ? "    " : "│   ");             tree += generate_tree(entries[i], new_prefix);         }     }     return tree; }  // Проверка, является ли файл бинарным bool is_binary(const fs::path& file_path) {     try {         std::ifstream file(file_path, std::ios::binary);         if (!file) return true;         char buffer[1024];         while (file.read(buffer, sizeof(buffer))) {             for (int i = 0; i < file.gcount(); ++i) {                 if (static_cast<unsigned char>(buffer[i]) < 32 &&                      buffer[i] != '\n' && buffer[i] != '\r' && buffer[i] != '\t') {                     return true;                 }             }         }         return false;     } catch (const std::exception& e) {         std::cerr << "Error checking binary file " << file_path << ": " << e.what() << '\n';         return true;     } }  // Обработка файла и добавление его содержимого в output void process_file(const fs::path& file_path, std::ostringstream& oss) {     try {         std::ifstream file(file_path, std::ios::in);         if (!file.is_open()) {             std::cerr << "Failed to open file: " << file_path << '\n';             return;         }         oss << file_path.filename().string() << "```\n";         oss << std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()) << "\n```\n\n";     } catch (const std::exception& e) {         std::cerr << "Error processing file " << file_path << ": " << e.what() << '\n';     } }  // Обработка группы файлов в одном потоке void process_file_chunk(const std::vector<fs::path>& files, std::ostringstream& oss) {     for (const auto& file_path : files) {         process_file(file_path, oss);     } }  // Основная функция для многопоточной обработки файлов std::string process_files_multithreaded(const fs::path& path, int num_threads) {     std::vector<fs::path> files_to_process;     try {         for (const auto& entry : fs::recursive_directory_iterator(path)) {             if (entry.is_regular_file()) {                 fs::path file_path = entry.path();                 if (file_path.parent_path().filename() == ".git") continue;                 if (!is_binary(file_path)) {                     files_to_process.push_back(file_path);                 }             }         }     } catch (const std::exception& e) {         std::cerr << "Error traversing directory " << path << ": " << e.what() << '\n';     }     std::sort(files_to_process.begin(), files_to_process.end());      // Разделение файлов на chunks для многопоточной обработки     std::vector<std::vector<fs::path>> chunks(num_threads);     int chunk_size = files_to_process.size() / num_threads;     int remainder = files_to_process.size() % num_threads;     int start = 0;     for (int i = 0; i < num_threads; ++i) {         int current_chunk_size = chunk_size + (i < remainder ? 1 : 0);         chunks[i] = std::vector<fs::path>(files_to_process.begin() + start, files_to_process.begin() + start + current_chunk_size);         start += current_chunk_size;     }      // Многопоточная обработка     std::vector<std::ostringstream> thread_outputs(num_threads);     std::vector<std::thread> threads;     for (int i = 0; i < num_threads; ++i) {         threads.emplace_back([&, i]() {             process_file_chunk(chunks[i], thread_outputs[i]);         });     }     for (auto& thread : threads) {         thread.join();     }      // Объединение результатов     std::ostringstream final_output;     for (auto& oss : thread_outputs) {         final_output << oss.str();     }     return final_output.str(); }  int main(int argc, char* argv[]) {     if (argc != 2) {         std::cerr << "Usage: " << argv[0] << " <path_to_traverse>\n";         return 1;     }     fs::path path(argv[1]);     if (!fs::exists(path) || !fs::is_directory(path)) {         std::cerr << "The provided path is not a directory or doesn't exist.\n";         return 1;     }      // Генерация дерева директорий     std::string tree = generate_tree(path);      // Обработка файлов с использованием многопоточности     int num_threads = std::thread::hardware_concurrency();     if (num_threads <= 0) num_threads = 4; // Fallback to 4 threads if hardware_concurrency() returns 0     std::string output = process_files_multithreaded(path, num_threads);      // Объединение дерева и содержимого файлов     std::string final_output = tree + '\n' + output;      // Запись результата в файл prompt.txt     fs::path prompt_file = path.parent_path() / "prompt.txt";     std::ofstream f(prompt_file);     if (!f.is_open()) {         std::cerr << "Error writing to prompt.txt\n";         return 1;     }     f.write(final_output.c_str(), final_output.size());     f.close();     std::cout << "prompt.txt has been created at " << prompt_file << '\n';      // Подсчет токенов в prompt.txt     std::ifstream infile(prompt_file);     if (!infile.is_open()) {         std::cerr << "Error reading prompt.txt for token counting\n";         return 1;     }     std::string content((std::istreambuf_iterator<char>(infile)), std::istreambuf_iterator<char>());     infile.close();      // Упрощенный подсчет токенов (по пробелам)     std::size_t token_count = 0;     bool in_token = false;     for (char c : content) {         if (std::isspace(static_cast<unsigned char>(c))) {             if (in_token) {                 ++token_count;                 in_token = false;             }         } else {             in_token = true;         }     }     if (in_token) ++token_count;      std::cout << "Number of tokens in prompt.txt: " << token_count << '\n';     return 0; }

Как код был скомпилирован под Windows в WSL при помощи MinGW64:

x86_64-w64-mingw32-g++ -static-libgcc -static-libstdc++ -o main64.exe cpp.cpp

После подготовки кодовой базы необходимо посчитать токены, для этого будем использовать токенизатор от OpenAI — Tiktoken. Cookbook от OpenAI по тому, как считать токены с помощью Tiktoken, утверждает, что данная библиотека используется в моделях вплоть до GPT-4o. Написан он на Python и Rust, что обеспечивает высокую производительность и быструю токенизацию в совокупности с удобством использования приятного синтаксиса Python. Использовалась кодировка токенизатора o200k_base, которую используют модели GPT-4o и GPT-4o-mini от OpenAI.

Исходники:

Простой код на Python. Почти полностью идентичен коду с OpenAI Cookbook, в том числе на котором, вполне возможно, обучали DeepSeek
import tiktoken  def count_tokens(file_path):     with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:         content = f.read()     encoding = tiktoken.encoding_for_model('gpt-4o')  # Используется в GPT-4o     tokens = encoding.encode(content)     return len(tokens)  token_count = count_tokens("prompt.txt") print(f"Number of tokens: {token_count}")

Итого, процесс выглядит так:

  • Скачивание кодовой базы

  • Её подготовка

  • Подсчёт токенов

Пример работы этих двух программ при обработке их же исходников

prompt.txt
├── cpp.cpp └── main.py  cpp.cpp``` #include <filesystem> #include <fstream> #include <string> #include <vector> #include <iostream> #include <cstddef> #include <algorithm> #include <cctype> #include <thread> #include <mutex> #include <sstream> #include <atomic> #include <memory>  namespace fs = std::filesystem;  // Генерация дерева директорий std::string generate_tree(const fs::path& path, const std::string& prefix = "") {     std::string tree;     std::vector<fs::path> entries;     for (const auto& entry : fs::directory_iterator(path)) {         if (entry.path().filename() == ".git") continue;         entries.push_back(entry.path());     }     std::sort(entries.begin(), entries.end(), [](const fs::path& a, const fs::path& b) {         return a.filename() < b.filename();     });     for (size_t i = 0; i < entries.size(); ++i) {         bool is_last = (i == entries.size() - 1);         std::string connector = is_last ? "└── " : "├── ";         if (fs::is_directory(entries[i])) {             tree += prefix + connector + entries[i].filename().string() + "/\n";         } else {             tree += prefix + connector + entries[i].filename().string() + "\n";         }         if (fs::is_directory(entries[i])) {             std::string new_prefix = prefix + (is_last ? "    " : "│   ");             tree += generate_tree(entries[i], new_prefix);         }     }     return tree; }  // Проверка, является ли файл бинарным bool is_binary(const fs::path& file_path) {     try {         std::ifstream file(file_path, std::ios::binary);         if (!file) return true;         char buffer[1024];         while (file.read(buffer, sizeof(buffer))) {             for (int i = 0; i < file.gcount(); ++i) {                 if (static_cast<unsigned char>(buffer[i]) < 32 &&                      buffer[i] != '\n' && buffer[i] != '\r' && buffer[i] != '\t') {                     return true;                 }             }         }         return false;     } catch (const std::exception& e) {         std::cerr << "Error checking binary file " << file_path << ": " << e.what() << '\n';         return true;     } }  // Обработка файла и добавление его содержимого в output void process_file(const fs::path& file_path, std::ostringstream& oss) {     try {         std::ifstream file(file_path, std::ios::in);         if (!file.is_open()) {             std::cerr << "Failed to open file: " << file_path << '\n';             return;         }         oss << file_path.filename().string() << "```\n";         oss << std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()) << "\n```\n\n";     } catch (const std::exception& e) {         std::cerr << "Error processing file " << file_path << ": " << e.what() << '\n';     } }  // Обработка группы файлов в одном потоке void process_file_chunk(const std::vector<fs::path>& files, std::ostringstream& oss) {     for (const auto& file_path : files) {         process_file(file_path, oss);     } }  // Основная функция для многопоточной обработки файлов std::string process_files_multithreaded(const fs::path& path, int num_threads) {     std::vector<fs::path> files_to_process;     try {         for (const auto& entry : fs::recursive_directory_iterator(path)) {             if (entry.is_regular_file()) {                 fs::path file_path = entry.path();                 if (file_path.parent_path().filename() == ".git") continue;                 if (!is_binary(file_path)) {                     files_to_process.push_back(file_path);                 }             }         }     } catch (const std::exception& e) {         std::cerr << "Error traversing directory " << path << ": " << e.what() << '\n';     }     std::sort(files_to_process.begin(), files_to_process.end());      // Разделение файлов на chunks для многопоточной обработки     std::vector<std::vector<fs::path>> chunks(num_threads);     int chunk_size = files_to_process.size() / num_threads;     int remainder = files_to_process.size() % num_threads;     int start = 0;     for (int i = 0; i < num_threads; ++i) {         int current_chunk_size = chunk_size + (i < remainder ? 1 : 0);         chunks[i] = std::vector<fs::path>(files_to_process.begin() + start, files_to_process.begin() + start + current_chunk_size);         start += current_chunk_size;     }      // Многопоточная обработка     std::vector<std::ostringstream> thread_outputs(num_threads);     std::vector<std::thread> threads;     for (int i = 0; i < num_threads; ++i) {         threads.emplace_back([&, i]() {             process_file_chunk(chunks[i], thread_outputs[i]);         });     }     for (auto& thread : threads) {         thread.join();     }      // Объединение результатов     std::ostringstream final_output;     for (auto& oss : thread_outputs) {         final_output << oss.str();     }     return final_output.str(); }  int main(int argc, char* argv[]) {     if (argc != 2) {         std::cerr << "Usage: " << argv[0] << " <path_to_traverse>\n";         return 1;     }     fs::path path(argv[1]);     if (!fs::exists(path) || !fs::is_directory(path)) {         std::cerr << "The provided path is not a directory or doesn't exist.\n";         return 1;     }      // Генерация дерева директорий     std::string tree = generate_tree(path);      // Обработка файлов с использованием многопоточности     int num_threads = std::thread::hardware_concurrency();     if (num_threads <= 0) num_threads = 4; // Fallback to 4 threads if hardware_concurrency() returns 0     std::string output = process_files_multithreaded(path, num_threads);      // Объединение дерева и содержимого файлов     std::string final_output = tree + '\n' + output;      // Запись результата в файл prompt.txt     fs::path prompt_file = path.parent_path() / "prompt.txt";     std::ofstream f(prompt_file);     if (!f.is_open()) {         std::cerr << "Error writing to prompt.txt\n";         return 1;     }     f.write(final_output.c_str(), final_output.size());     f.close();     std::cout << "prompt.txt has been created at " << prompt_file << '\n';      // Подсчет токенов в prompt.txt     std::ifstream infile(prompt_file);     if (!infile.is_open()) {         std::cerr << "Error reading prompt.txt for token counting\n";         return 1;     }     std::string content((std::istreambuf_iterator<char>(infile)), std::istreambuf_iterator<char>());     infile.close();      // Упрощенный подсчет токенов (по пробелам)     std::size_t token_count = 0;     bool in_token = false;     for (char c : content) {         if (std::isspace(static_cast<unsigned char>(c))) {             if (in_token) {                 ++token_count;                 in_token = false;             }         } else {             in_token = true;         }     }     if (in_token) ++token_count;      std::cout << "Number of tokens in prompt.txt: " << token_count << '\n';     return 0; } ```  main.py``` import tiktoken  def count_tokens(file_path):     with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:         content = f.read()     encoding = tiktoken.encoding_for_model('gpt-4o')  # Используется в GPT-4     tokens = encoding.encode(content)     return len(tokens)  token_count = count_tokens("prompt.txt") print(f"Number of tokens: {token_count}") ```  

Результат: Number of tokens: 1841. В целом это очень мало токенов, поэтому LLM и справилась с написанием такого рода примитивного, хоть и эффективного, софта.

(Не) Чистота эксперимента

Конечно, организовать структуру файла prompt.txt можно разными способами, что влияет на количество токенов и добавляет/убирает «шумы» — данные, которые к исходному коду напрямую не относятся. Но в любом случае меня интересует скорее порядок и оценка чисел, чем какие-то конкретные значения. Ядро Линукс содержит около 30 млн строк кода, соответственно, — число токенов там огромно, а при подобном подсчёте (организация кодовой базы в файл, в начале которого также находится её дерево, а содержимое каждого файла отделено от содержимого других файлов, и указаны все имена) добавляется около 10 млн строк «шума» и количество строк возрастает до 40 млн кратно количеству файлов в кодовой базе и, соответственно, токенов тоже становится больше, причем повторяющихся токенов, но такая организация как бы позволяет взглянуть на кодовую базу с высоты птичьего полёта — всё как на ладони и даже читабельно для человека. Ещё в кодовую базу могут попадать какие-нибудь не относящиеся к ней файлы и директории вроде .github, .idea, .vscode и прочих. Так что всё на правах for fun.

Результаты

Все вычисления были выполнены за разумное время, исчисляемое минутами, а не часами, что не может не радовать. Однако, изначально планировалось посчитать токены для исходников 11 проектов, но в совокупности со временем загрузки их из интернета и распаковки из архивов это затянулось на долго, даже если не выполнять примитивный подсчёт «токенов» в программе на C++, а только создавать итоговый файл. Возможно, что-то ещё добавится.

  • MySQL — 242 876 263

  • VS Code — 31 062 093

  • Blender — 82 885 995

  • Linux* — 456 479 607

  • LLVM* — 631 112 839

Выводы

Абсолютно бесполезная, но интересная информация.


ссылка на оригинал статьи https://habr.com/ru/articles/875022/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *