Считаем количество токенов для LLM в исходниках ядра Linux и не только…. ai.. ai. fun.. ai. fun. llm.. ai. fun. llm. openai.. ai. fun. llm. openai. tiktoken.. ai. fun. llm. openai. tiktoken. token.. ai. fun. llm. openai. tiktoken. token. tokenizer.. ai. fun. llm. openai. tiktoken. token. tokenizer. Инфографика.. ai. fun. llm. openai. tiktoken. token. tokenizer. Инфографика. искусственный интеллект.. ai. fun. llm. openai. tiktoken. token. tokenizer. Инфографика. искусственный интеллект. Программирование.. ai. fun. llm. openai. tiktoken. token. tokenizer. Инфографика. искусственный интеллект. Программирование. Софт.

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

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

  • MySQL

  • VS Code

  • Blender

  • Linux*

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

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

Сначала скачиваем репозиторий с исходниками, желательно удаляем папку .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```nn

Также эта программа выводит число «слов» в кодовой базе — простой подсчёт по разделению кодовой базы по пробелам и переносам строк, это число намного меньше, чем число токенов, подсчитанное продвинутым токенизатором от 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```nn";
    } 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.txtn";
        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 countingn";
        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```nn";
    } 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.txtn";
        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 countingn";
        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++, а только создавать итоговый файл. Возможно, что-то ещё добавится.

  • MySQL242 876 263

  • VS Code31 062 093

  • Blender82 885 995

  • Linux* — 456 479 607

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

Выводы

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

Автор: MainEditor0

Источник

Rambler's Top100