Считаем количество токенов для 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

Выводы

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

© Habrahabr.ru