Считаем количество токенов для 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 со следующей структурой:

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

Исходники:

Дисклеймер
Код на 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

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

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

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++, а только создавать итоговый файл. Возможно, что-то ещё добавится.

Выводы

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

Источник: https://habr.com/ru/articles/875022/


Интересные статьи

Интересные статьи

Автор: Денис ЗакусилоПриветствую всех неравнодушных! Это заключительная статья цикла о переходе от модульной архитектуры к сервисам.[Записки тимлида] Битрикс: от модулей к сервисам[Записки тимлида] Би...
Настройка и интеграция автоматической отправки SMS в ответ на события вроде получения заявки от клиента –– трудоемкая задача, но в этой статье наглядно покажем, как решить ее быстро и просто, и подели...
В последнее время на производственных площадках нефтепереработки и нефтехимии все чаще стало звучать словосочетание «Виртуальный анализатор». Что же это такое? Давайте разберемся. Виртуальный анализа...
На алгоритмической арене будет дано новое представление. Под куполом "Искусственного интеллекта" покажут потолок своих возможностей "Языки программирования" в столкновении с ограни...
Если вы последние лет десять следите за обновлениями «коробочной версии» Битрикса (не 24), то давно уже заметили, что обновляется только модуль магазина и его окружение. Все остальные модули как ...