Считаем количество токенов для 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-подобном формате, где начало и конец содержимого файлов обозначается тремя грависами
`
:```\n \n```\n\n
Также эта программа выводит число «слов» в кодовой базе — простой подсчёт по разделению кодовой базы по пробелам и переносам строк, это число намного меньше, чем число токенов, подсчитанное продвинутым токенизатором от OpenAI, о котором далее. Эту часть программы следовало бы убрать и сделать это достаточно легко, но можно просто прерывать процесс выполенения в терминале.
Исходники:
Дисклеймер
Автор не умеет писать на C++, весь код сгенерирован LLM и толком не прочитан, только проверена его работоспособность. Хоть в коде и игнорируется папка гита, но тем не менее файлы из неё попадают в итоговый файл, поэтому я и написал, что желательно её удалить, если она есть. Хотя в случае скачивания исходников с GitHub в zip-архиве (кнопка code → download zip) её нет.
Код на C++, который я бегло просмотрел и почти не читал
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace fs = std::filesystem;
// Генерация дерева директорий
std::string generate_tree(const fs::path& path, const std::string& prefix = "") {
std::string tree;
std::vector 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(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(file)), std::istreambuf_iterator()) << "\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& 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 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> 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(files_to_process.begin() + start, files_to_process.begin() + start + current_chunk_size);
start += current_chunk_size;
}
// Многопоточная обработка
std::vector thread_outputs(num_threads);
std::vector 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] << " \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(infile)), std::istreambuf_iterator());
infile.close();
// Упрощенный подсчет токенов (по пробелам)
std::size_t token_count = 0;
bool in_token = false;
for (char c : content) {
if (std::isspace(static_cast(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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace fs = std::filesystem;
// Генерация дерева директорий
std::string generate_tree(const fs::path& path, const std::string& prefix = "") {
std::string tree;
std::vector 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(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(file)), std::istreambuf_iterator()) << "\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& 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 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> 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(files_to_process.begin() + start, files_to_process.begin() + start + current_chunk_size);
start += current_chunk_size;
}
// Многопоточная обработка
std::vector thread_outputs(num_threads);
std::vector 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] << " \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(infile)), std::istreambuf_iterator());
infile.close();
// Упрощенный подсчет токенов (по пробелам)
std::size_t token_count = 0;
bool in_token = false;
for (char c : content) {
if (std::isspace(static_cast(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
Выводы
Абсолютно бесполезная, но интересная информация.