惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
IT之家
IT之家
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Security Archives - TechRepublic
Security Archives - TechRepublic
P
Proofpoint News Feed
C
CERT Recently Published Vulnerability Notes
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
P
Palo Alto Networks Blog
Know Your Adversary
Know Your Adversary
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Cisco Talos Blog
Cisco Talos Blog
L
Lohrmann on Cybersecurity
AWS News Blog
AWS News Blog
J
Java Code Geeks
博客园_首页
Scott Helme
Scott Helme
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
T
The Exploit Database - CXSecurity.com
Security Latest
Security Latest
V
Visual Studio Blog
Cloudbric
Cloudbric
Jina AI
Jina AI
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
A
Arctic Wolf
C
Cybersecurity and Infrastructure Security Agency CISA
S
SegmentFault 最新的问题
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
W
WeLiveSecurity
K
Kaspersky official blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hacker News: Ask HN
Hacker News: Ask HN
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
量子位
Google Online Security Blog
Google Online Security Blog
博客园 - Franky
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 三生石上(FineUI控件)
Recent Commits to openclaw:main
Recent Commits to openclaw:main

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Обучение LLM с нуля на c# и экспорт в GGUF. OpenCL против CUDA
virex · 2026-04-18 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение23 мин

Охват и читатели8.7K

Обзор

Статья о том как сделать свою ИИ модель на своих диалогах меньше чем за 10 минут, и экспортировать в gguf для запуска в LM Studio, Ollama, KoboldCpp, Jan, Oobabooga, llama.cpp.

Python не нужен. Мощная видеокарта NVIDIA не нужна, достаточно встройки. Хотя если у вас есть видеокарта NVIDIA - в программе можно указать для обучения CUDA.

В конце статьи ссылка на github с готовой программой и gguf моделью 442 КБ.

Нигде не видел что кто-то обучает модели на OpenCL, обычно это делают с помощью CUDA. Вот мы и попробуем использовать для этих целей давно позабытую технологию OpenCL.

Не переключайтесь...

У меня нет мощной видеокарты от NVidia. Соответственно нет CUDA мощи, принятой в качестве стандарта в машинном обучении (ML - Machine Learning). Но есть желание сделать что то похожее на нейронку.

На просторах интернета был найден пост о реализации простой llm на Rust. Сам проект находится на github: tekaratzas/RustGPT.

Т.к. моих знаний и опыта не хватит для самостоятельного разбора исходного кода на Rust, и переписывать на c# логику - за пределами моих возможностей, в помощь были привлечены все доступные бесплатные LLM (Qwen CLI, Gemini CLI, Kilo-code, Perplexy, и даже ИИ режим гугла в браузере).

Теория

В статье Алиса, подвинься, я уже писал немного теории в разделе "LLM, БЯМ, Нейронка, Чат-гпт. Краткий понятийный курс":

Фактически нейронка — это массив чисел загруженный в оперативную память, которому подают на вход какую-либо информацию (текст, фото, звук), и эта информация, проходя через массив, выдает определенный результат, тоже в виде массива чисел.

Что собственно необходимо сделать что бы обучить llm? И что значит "обучить"?

Представим что модель - это конвейер, через который проходит и видоизменяется информация.

  1. Токенизация. Так как на вход нейронки нельзя просто передать слова (а так же изображение, звук), ей нужно передать массив чисел, которые мы "закрепим" за определенной единицей информации. Проще говоря: за словом "привет" можно застолбить число 1375, за пробелом число 333, а за словом "мир" - 777. Многократно передав эти числа (токены) в нейронку - мы обучим её так, что бы она могла запомнить эту последовательность: 1375, 333, 777 (то есть словосочетание "привет мир"). Так вот, токенизация - это разбитие информации на части. Можно разбить предложения по словам ("привет"), можно по символам ("п|р|и|в|е|т"), а можно выбрать золотую середину ("прив|ет|").
    Кроме слов, в предложениях существуют и знаки препинания, управляющие "потоком мысли" предложения. Они тоже - отдельные токены.

  2. Embedding слой имеет связь между токеном его числовым описанием (вектором) хранящемся в весах. Изначально до обучения, в весах - полный хаос.

  3. TransformerBlock - это блок, который при получении последовательности векторов, преобразует эти последовательности в связи, и передает их следующему блоку (так же в виде последовательности векторов). Фактически он получает вектора, делает их "умнее", и передает дальше.
    Упрощенно он состоит из следующих частей:

    1. Self-attention - механизм "внимания" связывающий слова (т.е. векторы) между собой, через четыре Linear слоя:

      1. Query ("запрос") - веса, которые формулируют вопрос от текущего слова к остальным

      2. Key ("ключ") - веса, которые описывают характеристики слова для других

      3. Value ("значение") - сама суть, содержательная информация слова

      4. Output ("проекция") - веса, которые собирают результаты всех "голов" внимания (если их несколько) обратно в один вектор.

        Query и Key решают, кто с кем связан.
        Value дает ответ на вопрос что именно передать по этой связи.
        Output упаковывает результат для передачи дальше.

    2. FeedForward - небольшая "сеть", концентрирующая информацию полученную от Self-attention, для следующего слоя. Здесь хранятся "знания" и "факты". Состоит из двух Linear слоев.

    3. Layer normalization - "выравнивание" чисел после обучения. Содержит небольшие веса (обычно gamma и beta) помогающие модели удерживать числа в рабочем диапазоне.

    4. Residual connection - делает "поправку" с тем что было на входе и с тем что выходит из предыдущих частей: складывает числа на входе и на выходе, что бы не потерять исходную информацию.

  4. Output projection - последний Linear слой, переводящий внутреннее состояние модели в вероятность следующего слова. Содержит веса решающие, какое именно слово из 50 000 вариантов станет следующим.

То есть главная задача данного конвейера (нейронки/модели) - предсказать какое слово будет дальше, если ему на вход подать начальное слово. Да, этакий T9, но имеющий контекст и понимающий смысл текста.

Как происходит обучение:

  1. Pre-processing: преобразуем подготовленные данные (dataset) в токены.

  2. Разделение обучающих данных на "вход" и "выход" для обучения предсказанию. Например фраза "Мама мыла раму" (токены: 111, 555, 777). Вход: "Мама мыла" (токены: 111, 555), выход: "мыла раму" (токены: 555, 777).

  3. Forward Pass: передача конвейеру "вход" ("Мама мыла", токены: 111, 555). Данные бегут через все слои (Embedding -> Attention -> FeedForward -> Layer normalization -> Residual connection). На выходе получаем Logits - сырые оценки вероятности для каждого слова из словаря.

  4. Оценка ошибки Loss: сравнивается вероятность Logits с "выходом", и чем меньше вероятность с "выходом" тем больше число loss. На основе loss мы вычисляем градиент (Backpropagation) - степень виновности каждого веса в ошибке.

    1. Последний слой (FeedForward) спрашивает: "На сколько мои веса w2 виноваты в этой ошибке?". Ответ записывается в градиент для w2.

    2. Затем этот слой передает сигнал ошибки назад предыдущему слою (SelfAttention). SelfAttention делает то же самое: вычисляет, как его матрицы wQ, wK, wV повлияли на итоговый провал.

  5. ClipGradients: если градиент ("виновность") слишком велик, модель может "психануть" и слишком сильно изменить веса, всё испортив. Поэтому прежде чем обновить веса, мы делаем "обрезку".

  6. Optimizer обновляет веса во всех слоях (в Embeddings, SelfAttention, и в FeedForward) на основе градиента:

    1. Если градиент положительный: Значит, увеличение веса увеличивает ошибку. Мы вычитаем его, чтобы уменьшить вес.

    2. Если градиент отрицательный: Значит, увеличение веса уменьшит ошибку. Минус на минус дает плюс — мы увеличиваем вес.

    3. Если градиент равен 0: Мы в идеальной точке (или в тупике), менять ничего не нужно.

      У оптимизатора есть некий рычаг управления Learning Rate (LR) - коэффициент скорости обучения. Если градиент показывает направление (увеличивать или уменьшать вес), то LR - силу с которой надо это делать. Высокий LR - модель делает большие шаги в обучении, но может проскочить идеальную "обученность" и Loss будет прыгать, так и не обучив модель до конца. Низкий LR - модель делает мелкие шаги в обучении, медленно учится, и может застрять на определенном уровне, так как ей не хватит преодолеть "локальный минимум" Loss.

Первая версия

Бесплатный ИИ сделал перенос кода на c# сразу с использованием библиотеки MathNet.

Данные для обучения:

pretrain.json
[
    "The sun rises in the east and sets in the west </s>",
    "Water flows downhill due to gravity </s>",
    "Birds fly through the air using their wings </s>",
    "Fish swim in rivers, lakes, and oceans </s>",
    "Trees grow tall and produce leaves </s>",
    "Rain falls from clouds in the sky </s>",
    "Fire is hot and produces light </s>",
    "Ice is frozen water that melts when heated </s>",
    "Mountains are tall and rocky formations </s>",
    "The moon orbits around planet Earth </s>",
    "Flowers bloom in spring and summer </s>",
    "Snow is cold and white </s>",
    "Wind moves air from place to place </s>",
    "Rivers flow into larger bodies of water </s>",
    "Sand is found on beaches and in deserts </s>",
    "Grass grows in fields and yards </s>",
    "Rocks are hard and can be different colors </s>",
    "Stars shine bright in the night sky </s>",
    "Waves move across the surface of water </s>",
    "Clouds form when water vapor rises </s>",
    "Lightning is bright and makes thunder </s>",
    "Storms bring rain and strong winds </s>",
    "Seasons change throughout the year </s>",
    "Animals eat food to survive </s>",
    "Plants need sunlight and water to grow </s>"
]
tune.json
[
    "User: What causes rain? Assistant: Rain is caused by water vapor in clouds condensing into droplets that become too heavy to remain airborne </s>",
    "User: How do mountains form? Assistant: Mountains are formed through tectonic forces or volcanism over long geological time periods </s>",
    "User: What is the Amazon rainforest? Assistant: The Amazon rainforest is one of the most biodiverse places on earth, home to countless species </s>",
    "User: At what temperature does water boil? Assistant: Water boils at 100 degrees celsius at standard atmospheric pressure </s>",
    "User: How long does it take the moon to orbit Earth? Assistant: The moon orbits the earth approximately every 27.3 days </s>",
    "User: What is photosynthesis? Assistant: Photosynthesis is the process by which green plants use sunlight to synthesize food from carbon dioxide </s>",
    "User: How does gravity work? Assistant: Gravity is a force that attracts two bodies toward each other based on their mass </s>",
    "User: How many neurons are in the human brain? Assistant: The human brain contains about 86 billion neurons that transmit information throughout the body </s>",
    "User: What is electricity? Assistant: Electricity is the flow of electrons through a conductor, often used to power devices </s>",
    "User: What is climate change? Assistant: Climate change refers to long-term shifts in temperatures and weather patterns on Earth </s>",
    "User: How long do oak trees live? Assistant: Oak trees can live for hundreds of years and produce acorns as their fruit </s>",
    "User: What happened to Pluto? Assistant: Pluto was reclassified from a planet to a dwarf planet in 2006 by astronomers </s>",
    "User: How is glass made? Assistant: Glass is made by heating sand, soda ash, and limestone to very high temperatures until they melt </s>"
]

Pretrain - общие сведения о мире: "солнышко блестит, дождик капает". Tune - псевдо обучение диалогу. Но на такой маленькой модели, после дообучения (Tune), "знания" натренированные с помощью Pretrain - "перезатираются".

Program.cs
//данные для обучения
var pretrain = JsonConvert.DeserializeObject<List<string>>(File.ReadAllText("pretrain.json"));
var tune = JsonConvert.DeserializeObject<List<string>>(File.ReadAllText("tune.json"));

//разбиваем на токены
var vocabSet = new HashSet<string>();

Vocab.ProcessTextForVocab(pretrain, vocabSet);
Vocab.ProcessTextForVocab(tune, vocabSet);

var vocabWords = vocabSet.ToList();
vocabWords.Sort();
var vocab = new Vocab(vocabWords);

//вычисляем оптимальную максимальную длину последовательности на основе данных
//maxSeqLen - это фактически размер контекста в токенах
var allTexts = pretrain.Concat(tune);
var maxSeqLen = DatasetLoader.CalculateOptimalMaxSeqLen(allTexts, vocab);
Console.WriteLine($"Вычислена оптимальная MAX_SEQ_LEN: {maxSeqLen}");

//первый Embedding слой
var embeddings = new Embeddings(vocab, maxSeqLen);

//размер матрицы
var embeddingDim = 128;
//размер матрицы внутреннего слоя в Feed-forward
var hiddenDim = 256;

//трансформеры
var transformerBlock1 = new TransformerBlock(embeddingDim, hiddenDim);
var transformerBlock2 = new TransformerBlock(embeddingDim, hiddenDim);
var transformerBlock3 = new TransformerBlock(embeddingDim, hiddenDim);

//выходной слой
var outputProjection = new OutputProjection(embeddingDim, vocab.Words.Count);

//создаем "нейронку" - конвейер из блоков
var llm = new LLM(vocab, maxSeqLen, new List<ILayer>
{
embeddings,
transformerBlock1,
transformerBlock2,
transformerBlock3,
outputProjection
});

//обучаем общим знаниям 10 эпох
Console.WriteLine("=== PRE-TRAINING MODEL ===");
llm.Train(pretrain, 10, 0.0005f);

//инференс
//исходное предложение: "Flowers bloom in spring and summer"
//пишем первые слова из предложения:
//"Flowers bloom"
//модель должна продолжить текст
//"in spring and summer </s>"
var text1 = "Flowers bloom";
var result1 = llm.PredictWithConfidence(text1);
Console.WriteLine($"Text: {text1}");
Console.WriteLine($"LLM: {result1.Text}");

//обучаем псевдодиалогу 10 эпох
Console.WriteLine("=== INSTRUCTION TUNING ===");
llm.Train(tune, 10, 0.0001f);

//инференс
//проверяем
//модель должна продолжить текст
//Assistant : The human brain contains about 86 billion neurons that transmit information throughout the body </s>
var text2 = "User: How many neurons are in the human brain?";
var result2 = llm.PredictWithConfidence(text2);
Console.WriteLine($"Text: {text2}");
Console.WriteLine($"LLM: {result2.Text}");
Результат

Уже тут видно, как нейронка - просто дописывает за пользователем предложения, на которых обучена. Т9 во всей красе.

Но какой потенциал! Это уже хоть какая-то говорилка! Самая маленькая текстовая нейросеть!

Параметр maxSeqLen (Maximum Sequence Length) - это тот самый размер контекста в токенах! Это максимально допустимая длина текста (количество токенов), которую модель может обработать за один раз. В программе параметр вычисляется автоматически, но никто не мешает установить его намного больше вручную.

Ничто не мешает нам обучить модель на русском языке. Нужно только увеличить количество эпох так, что бы значение было примерно или ниже: Loss = 0,1850.

Общение с моделью на русском

В данном случае я передаю модели строку "User: " + текст набранный в консоли.

Модель запомнила строку:
"User: Как изготавливается стекло? Assistant: Стекло изготавливается путем нагревания песка, кальцинированной соды и известняка до очень высоких температур, пока они не расплавятся </s>"

Передаю модели строку:
"User: Как изготавливается стекло?

Модель выдаёт продолжение заученной строки:
"Assistant : Стекло изготавливается путем нагревания песка , кальцинированной соды и известняка до очень высоких температур , пока они не расплавятся </s>"

Заметили что перед запятыми и после них - пробелы? Модель выдает не точную копию исходного текста, а последовательность токенов, преобразованную обратно в слова.

Модель выдает токены (для нагладности добавил разделитель):
293|2|254|104|216|154|182|1|118|246|101|103|78|176|53|261|1|200|173|155|219|5

А потом эти токены преобразовываются в слова:
Assistant|:|Стекло|изготавливается|путем|нагревания|песка|,|кальцинированной|соды|и|известняка|до|очень|высоких|температур|,|пока|они|не|расплавятся|</s>

В коде после каждого токена добавляется пробел.

При инференсе, если дать модели слова которых она не знает, она будет выдавать мусор. Когда модель выдает нам готовый результат, мы можем вычислить "уверенность" с которой модель выдает каждый токен. И на основе этого определять: правильные ли токены она нам выдает, или нет.

Выдаем только "уверенный" ответ
// ждем от пользователя слова
Console.Write("Enter prompt: ");
var input = Console.ReadLine();

// запрос к модели, инференс
var formattedInput = $"User: {input.Trim()}";
var result = llm.PredictWithConfidence(formattedInput);

// выдаем только "уверенный" ответ
if (result.MinConfidence > 0.6f)
  Console.WriteLine($"Model output: {result.Text}");
else
  Console.WriteLine("Model output: Затрудняюсь ответить");

Модель можно сохранить и загрузить из .bin файла. Размер файла готовой модели llm_model.bin - 6,87 Мб (!).

Откапываем OpenCL

Обучать микро модель на центральном процессоре еще можно, но если хочется сделать более осмысленную модель, с бОльшими весами, вмещающую больше информации - терпения не хватит. Это долго, это шум кулера, это ужасно если долго ждал - а результата ноль и нужно переучивать заново.

Нейронка - это матрицы. Умножать, делить, складывать ...матрицы это тяжело для CPU. Нужна мощь видеокарты. Видеокарты заточены на операции с матрицами.

У меня нет видеокарты NVidia с CUDA процессорами на борту. У меня встройка от AMD. Да, у нее есть NPU модуль, но не у всех он есть. За то есть OpenCL.

OpenCL - это гетерогенная технология операций с матрицами. То, что CPU делает в один поток, OpenCL делает тысячами потоков параллельно.

OpenCL работает с 2009 года на GeForce 8000 (8800 GT), Radeon HD 4000,  Intel HD 4000.

Главная особенность OpenCL - расчеты ведутся в kernel. Кернел - это блок кода выполняющийся параллельно на нескольких ядрах. Примерно как шейдер для видеокарты, но не для графики, а для вычислений. Код пишется на языке OpenCL C (похож на C99).

...А ведь появление CUDA обязано пиксельным шейдерам для этих ваших игрулек. В 2003 студенты Стендфорда разработали проект BrookGPU, позволяющий производить расчёты сложных физических симулиций и прочего через шейдеры. Этот метод получил название GPGPU - General-Purpose computing on GPU, то есть "универсальные вычисления на графическом процессоре". Затем NVIDIA вдохновленная этим проектом наняла создателя BrookGPU Яна Бака, и придумала CUDA.

OpenCL появился в 2008 году уже как ответка всей остальной индустрии на монополию CUDA. Придумала его Apple, и пошло поехало...

ILGPU

На просторах интернета есть проект https://ilgpu.net. ILGPU это JIT-компилятор с открытым исходным кодом, который позволяет писать код для GPU (CUDA и OpenCL) прямо на обычном C#.

Есть проект на github: https://github.com/m4rs-mt/ILGPU.

Вот пример перемножения матриц на CPU (одна из рутинных операций при обучении нейронки):

float[,] a = new float[size, size];
float[,] b = new float[size, size];
float[,] res = new float[size, size];

//заполнение случайными данными
...

//умножение
for (int i = 0; i < size; i++)
{
    for (int j = 0; j < size; j++)
    {
      float sum = 0;
      for (int k = 0; k < size; k++)
      {
        sum += a[i, k] * b[k, j];
      }
      res[i, j] = sum;
    }
}

А теперь сравним с GPU OpenCL. Каждая матрица размерностью 1024 на 1024.

Program.cs
using ILGPU;
using ILGPU.Runtime;
using ILGPU.Runtime.OpenCL;
using MathNet.Numerics.LinearAlgebra;
using System.Diagnostics;

class Program
{
    // Кернел для GPU
    static void MultiplyKernel(
        Index2D index,
        ArrayView2D<float, Stride2D.DenseX> a,
        ArrayView2D<float, Stride2D.DenseX> b,
        ArrayView2D<float, Stride2D.DenseX> res)
    {
        float sum = 0;
        for (int k = 0; k < a.Extent.Y; k++)
            sum += a[new Index2D(index.X, k)] * b[new Index2D(k, index.Y)];
        res[index] = sum;
    }

    static void Main()
    {
        int size = 1024;

        Console.WriteLine($"Перемножение матриц. Размер матрицы: {size}x{size}");

        //1. CPU =====================================
        float[,] a = new float[size, size];
        float[,] b = new float[size, size];
        float[,] resCpu = new float[size, size];
        var sw = Stopwatch.StartNew();

        //заполнение случайными данными
        Random r = new Random();
        for (int i = 0; i < size; i++)
            for (int j = 0; j < size; j++)
            {
                a[i, j] = (float)r.NextDouble();
                b[i, j] = (float)r.NextDouble();
            }

        //умножение
        for (int i = 0; i < size; i++)
        {
            for (int j = 0; j < size; j++)
            {
                float sum = 0;
                for (int k = 0; k < size; k++)
                {
                    sum += a[i, k] * b[k, j];
                }
                resCpu[i, j] = sum;
            }
        }
        sw.Stop();
        Console.WriteLine($"1. CPU Время: {sw.Elapsed.TotalMilliseconds:F2} мс");


        // 2. MATH.NET ===============================
        var m_a = Matrix<float>.Build.DenseOfArray(a);
        var m_b = Matrix<float>.Build.DenseOfArray(b);
        sw.Restart();
        var m_res = m_a * m_b;
        sw.Stop();
        Console.WriteLine($"2. Math.NET (SIMD/Multi-thread): {sw.ElapsedMilliseconds} мс");


        //3. GPU =====================================
        using var context = Context.Create(builder => builder.OpenCL());
        using var accelerator = context.GetPreferredDevice(false).CreateAccelerator(context);

        var kernel = accelerator.LoadAutoGroupedStreamKernel<
            Index2D,
            ArrayView2D<float, Stride2D.DenseX>,
            ArrayView2D<float, Stride2D.DenseX>,
            ArrayView2D<float, Stride2D.DenseX>>(MultiplyKernel);

        var extent = new Index2D(size, size);
        using var bufA = accelerator.Allocate2DDenseX<float>(extent);
        using var bufB = accelerator.Allocate2DDenseX<float>(extent);
        using var bufRes = accelerator.Allocate2DDenseX<float>(extent);

        bufA.CopyFromCPU(a);
        bufB.CopyFromCPU(b);

        // Прогрев (исключаем JIT из замера)
        kernel(extent, bufA.View, bufB.View, bufRes.View);
        accelerator.Synchronize();

        // Чистый замер GPU
        sw.Restart();
        kernel(extent, bufA.View, bufB.View, bufRes.View);
        accelerator.Synchronize();
        sw.Stop();

        Console.WriteLine($"3. GPU Время (чистое): {sw.Elapsed.TotalMilliseconds:F2} мс");

        Console.WriteLine($"Устройство: {accelerator.Name}");
    }
}
Результат

Неплохо. Первый вариант нейронки уже работает на стероидах - библиотеке Math.NET использующей некислый хак от производителя процессора: инструкции SIMD (AVX, AVX2, AVX-512SSE2, FMA3).

Но это всё равно хуже OpenCL.

Как обычно, реальность немного приземляет. Последняя версия ILGPU 1.5.3 поддерживает устройства с OpenCL 2.0 и выше. Проверил на NVIDIA GeForce GTX 560 - библиотека ILGPU не хочет работать с видеокартой, т.к. старая видеокарта поддерживает только OpenCL 1.1.

Можно попробовать задействовать более старые версии библиотеки... Можно, а зачем?

Вторая версия. OpenCL

На этот раз я попросил ИИ перевести вычисления на OpenCL, т.е. задействовать библиотеку ILGPU для работы с матрицами.

Хитрый ИИ сделал так, что программа перегоняла данные в GPU, возвращала обратно, а бОльшую часть вычислениий производила на CPU. В диспетчере задач, во вкладке GPU, на графике "Copy" была большая активность. А в графике "Compute" - почти не заметная активность. В коде обилие методов "CopyFromCPU", "CopyToCPU".

WTF?

После многократных исправлений нагрузка перешла в график "Compute".

Другое дело!

Теперь при вычислениях CPU не нагружается вообще, вентилятор не шумит!

Mixture of Experts (MoE)

Поигравшись в рабочую версию, решил добавить блок MoE.

Если в "обычном" Transformer блоке структура такая: Layer Norm -> Self-Attention (+ Residual) -> Layer Norm -> FeedForward (+ Residual).

То в Transformer MoE (как в Mixtral): Layer Norm -> Self-Attention (+ Residual) -> Layer Norm -> MoE (Router -> FeedForward1 , FeedForward2...) (+ Residual).

Router - это легкий Linear слой, который анализирует вектор и "решает", каким именно FeedForward слоям (экспертам) его передать. Грубо говоря Router - это "диспетчер" выдающий вероятность выбора того или иного "эксперта". А FeedForward - эксперт.

Особых различий в обучении не заметил. Но это и понятно: корпус (текст) для обучения очень маленький.

GGUF

А почему бы собственно не экспортировать модель в формат gguf что бы любой софт использующий llama.cpp (Ollama, LM Studio и прочие) могли загрузить нашу модель?

Оказалось что не всё так просто. С наскока такое не провернуть. Конечно я пробовал экспортировать модель с такой конфигурацией: Embedding -> TransformerBlockMultiHead -> Linear. Но, llama.cpp выдавала ошибки типа: llama_model_load: error loading model: done_getting_tensors: wrong number of tensors; expected 37, got 33

Необходимо создать такую конфигурацию модели, какую понимает llama.cpp. Бесплатный ИИ предложил сделать архитектуру GPT-2: Embedding -> PositionalEmbedding -> TransformerBlockPreNorm -> LayerNorm -> Linear.

Токенизатор

Для полной совместимости с llama.cpp нужно было создать GPT-2 Byte-Level BPE (Byte Pair Encoding) токенизатор. Главная фишка такого токенизатора в том, что для него нет неизвестных символов, он может работать совершенно с любыми словами и символьными последовательностями. В старых токенизаторах (например BERT) если встречался неизвестный символ, модель выдавала [UNK] (Unknown). А BPE работает с базовым словарем на 256 символов и может их комбинировать как угодно. Это гарантирует, что любой файл или символ в любой кодировке может быть прочитан моделью. Он объединяет пары символов которые встречаются часто вместе - в один токен. Т.е. несколько часто встречающихся вместе токенов "п" "р" "и" "в" "е" "т" при анализе текста, объединится в один токен "привет". Фактически это означает что появится новый вектор (число) за которым будет закреплено слово "привет".

Encode - кодирование сырого текста в список токенов предоставленных в виде векторов (чисел).

Decode - преобразование токенов в текст.

Пример того как модель видит текст: [2, 4, 448, 7, 5]

Пример того как модель видит текст: [2, 4, 448, 7, 5]

Обучение токенизатора происходит в два этапа:

  • Разрезание текста на куски (pre-tokenization) по определенному регулярному выражению (Regex). Такое разрезание гарантирует, что буквы, цифры и знаки препинания не будут объединяться в один токен. Например "привет!" будет разбито на "привет" и "!".

  • Ну и собственно слияние (merges). Токенизатор ищет самую частую пару соседних байтов/токенов внутри нарезанных кусков текста и объединяет их в новый токен. Слияние происходит не сразу. В первый проход из "п" "р" "и" "в" "е" "т", произойдет слияние "пр" "и" "в" "е" "т", затем "при" "в" "е" "т" и т.д. А может и так: "при" "вет". Все зависит от корпуса.

У llama.cpp есть утилита показывающая как она интерпретирует запросы в своем токенизаторе. Команда: lama-tokenize -m model.gguf -p "<user> привет!<assistant>" --ids так же выдает [2, 4, 448, 7, 5]

Корпус

Корпус - это собственно текст на котором учится модель. Корпус можно взять готовый, например здесь: https://huggingface.co/datasets/d0rj/alpaca-cleaned-ru. А можно попросить любую крупную LLM сгенерировать вам свой корпус. Так можно получить одну из разновидностей Distilled (дистилляция) модели. Т.е. малая модель обучается на более концентрированных и чистых корпусах от большой умной модели.

В нашем случае, корпус нужно правильно оформить для обучения. Например строка:

"User: привет Assistant: привет! рада тебя видеть. чем могу помочь?"

для обучения преобразуется в такой вид:

"<s> <user> привет<assistant> привет! рада тебя видеть. чем могу помочь? </s>".

<s> - стартовый токен (Bos - Beginning of Sequence), позволяющий модели понять, что начинается новая генерация. Для большинства моделей этот токен позволяет сбросить контекст и настроить механизмы внимания (Attention) для первого слова.

</s> - конечный токен (Eos - End of Sequence). Модель может генерировать текст бесконечно. Если она запомнит этот токен при обучении, то программа которая будет делать инференс этой модели (llama.cpp) при получении этого заученного токена тут же завершит генерацию следующих токенов. Этот токен нужен больше для инференса.

В таком случае инференс преобразует вашу строку:

"привет"

в такое:

"<s> <user> привет<assistant>"

И модель получив незаконченную строку начинает генерировать недостающие последовательности токенов которые должны стоять друг за другом с высокой вероятностью:

" привет! рада тебя видеть. чем могу помочь? </s> ёлки палки какой-то мусор который не пропустит llama.cpp после eos токена й3зшо2хщ0шо2хй3щ"

Еще был такой токен <sep> (Separator) - для разделения строки на вопрос и ответ. Но у нас для этого есть  <user> и <assistant>.

Шаблон

Для llama.cpp очень важен шаблон по которому она будет преобразовывать ваши запросы к модели в правильные токены, и преобразовывать результат модели в текст.

Вот такой шаблон получился для нашей модели с нашим особенным корпусом:

string chatTemplateHistory =
    "{% for message in messages %}" +
    "{% if message['role'] == 'user' %}" +
    "<user> {{ message['content'] }}<assistant>" +
    "{% elif message['role'] == 'assistant' %}" +
    " {{ message['content'] }}</s>" +
    "{% endif %}" +
    "{% endfor %}";

Почему особенный корпус? Потому что наши Bos, Eos, User, Assistant токены нестандартные. Например в Llama3 Bos токен - это <|begin_of_text|>, в ChatML (Qwen, DeepSeek): <|im_start|>.

Такой шаблон будет генерировать растущий запрос к модели с каждым запросом пользователя. Вы напишете "привет", он отравит модели

"<s> <user> привет<assistant>"

Затем напишете "как дела?", он отправит:

"<s> <user> привет<assistant>привет! рада тебя видеть. чем могу помочь? </s> <user> как дела?<assistant>"

Но на самом деле llama.cpp держит предыдущий ответ в KV-кеше, и не пересылает всю эту огромную строку текста заново. При вводе нового запроса, llama.cpp дописывает его в свободные ячейки памяти кеша. Если контекст у модели маленький, llama.cpp начнет "выбрасывать" начало истории из кеша и инференс ломается.

Только вот моделька у нас не то что бы микро размеров, она ну очень маленькая, и не может переварить историю запросов. Ей нужно при каждом инференсе сбрасывать накопленные данные. Для этого мы схитрим, сделав такой шаблон:

//чат без истории
string chatTemplate =
    "{% if messages %}" +
    "{% set message = messages[-1] %}" + // только последнее сообщение
    "{% if message['role'] == 'user' %}" +
    "<user> {{ message['content'] }}<assistant>" +
    "{% endif %}" +
    "{% endif %}";

Тут надо озвучить одну особенность. Данный шаблон заработает если мы в метаданных экспорта в GGUF укажем такое (установим add_bos_token = true):

kvs.Add(MakeKV("tokenizer.ggml.add_bos_token", GgufType.Bool, true));
kvs.Add(MakeKV("tokenizer.ggml.add_eos_token", GgufType.Bool, false));

Если установить add_bos_token = false, тогда нам надо добавить Bos токен <s> в шаблон:

string chatTemplate =
    "<s>{% if messages %}" +
    "{% set message = messages[-1] %}" + // берем только последнее сообщение
    "{% if message['role'] == 'user' %}" +
    "<user> {{ message['content'] }}<assistant>" +
    "{% endif %}" +
    "{% endif %}";

Метаданные GGUF

Метаданные это фактически инструкция для llama.cpp как загружать модель в память, и как с ней работать.

general.architecture - gpt2. Если указали эту архитектуру, то для архитектуры добавляются специфичные поля:
gpt2.context_length - длина контекста
gpt2.embedding_length - размер Embedding слоя
gpt2.feed_forward_length - размер скрытого слоя
gpt2.attention.head_count - количество "голов"
gpt2.block_count - количество слоёв
и прочее.

general.file_type - точность, степень квантования: 32 бита - полная точность, F16, Q4_0 и т.д.

Токенизатор:
tokenizer.ggml.tokens - массив строк, включая <user>, <assistant>
tokenizer.ggml.merges - правила склейки
tokenizer.ggml.bos_token_id - наши знакомые bos
tokenizer.ggml.eos_token_id - и eos

Ну и конечно же шаблон: tokenizer.chat_template.

Запускаем в LM Studio

Итак. Прежде чем сделать инференс, нужно модель обучить. Давайте посмотрим на конфигурацию нашей модели:

  • EmbeddingDim = 64 (размер Embedding слоя для связи между токеном его числовым описанием (вектором))

  • HiddenDim = 128 (размер скрытого слоя)

  • NumHeads = 2 (количество "голов")

  • NumLayers = 1 (количество слоёв, т.е. трансформера TransformerBlockPreNorm)

  • VocabSize = 512 (размер словаря соответствий Токен <-> ID)

  • MaxSeqLen = 80 (длина контекста... который может переварить модель за один раз)

Итого параметров у модели: 103 744. Это где то примерно 0,000103744 миллиардов параметров... А размер model.gguf файла 422 Кб.

Вот что гугл сказал про такую конфигурацию

Особенности для вашей конфигурации:

  1. Плотность информации: При EmbeddingDim = 64 каждый токен описывается очень коротким вектором. Это значит, что модель сможет различать только самые базовые понятия. Сложные связи (ирония, глубокий контекст) ей будут недоступны.

  2. Головы вниманияNumHeads = 2 означает, что на каждую «голову» приходится всего по 32 измерения (64 / 2). Это необходимый минимум, чтобы модель могла одновременно следить, например, за подлежащим и сказуемым.

  3. Критичность параметров: При NumLayers = 1 у модели всего один шанс «понять» смысл текста. В глубоких моделях (Llama, GPT-4) десятки слоев уточняют смысл, а здесь модель работает как очень продвинутый Т9.

Обучение модели:

  1. Pretrain около 100 эпох с LR (learning rate) = 0.001

  2. Tune (обучение диалогам) около 100 эпох с LR=0.0005 (loss упадет с 8,4 до 0,3), затем еще несколько раз с меньшим LR но больше эпох, пока loss не достигнет 0.12. Это самое долгое.

Лог обучения (минуты 2-3) с инференсом в конце:
=== GPU INFO ===
Device: gfx1150
Type: OpenCL
Memory: 37357 MB
Max threads/group: 256
Warp size: 32
================

╔══════════════════════════════════════╗
║   LLM ILGPU — Главное меню           ║
╠══════════════════════════════════════╣
║ 1. Загрузить модель из файла         ║
║ 2. Создать новую GPT2 и обучить      ║
╚══════════════════════════════════════╝

Выберите опцию (1 или 2): 2

═══ СОЗДАНИЕ МИНИМАЛЬНОЙ GPT2 ═══
   Архитектура: GPT-2 (Pre-Norm, GELU, обучаемые positional)

═══ Обучение BpeTokenizer ═══
  [special] '<pad>' => 0
  [special] '<unk>' => 1
  [special] '<s>' => 2
  [special] '</s>' => 3
  [special] '<user>' => 4
  [special] '<assistant>' => 5
  [special] '<sep>' => 6
Базовый vocab (specials + bytes): 263
Уникальных слов: 324
step=   0 freq=  450 ' '+'�'=>' �'
step=  50 freq=   29 ' �'+'�'=>' м'
step= 100 freq=   13 ' '+'—'=>' —'
step= 150 freq=    7 'и'+'ть'=>'ить'
step= 200 freq=    5 'ен'+'и'=>'ени'
Готово: 249 слияний, vocab=512 [35ms]

[ВЕРИФИКАЦИЯ СПЕЦТОКЕНОВ]
  ✅ '<pad>' => id=0
  ✅ '<unk>' => id=1
  ✅ '<s>' => id=2
  ✅ '</s>' => id=3
  ✅ '<user>' => id=4
  ✅ '<assistant>' => id=5
  ✅ '<sep>' => id=6
  ✅ Все спецтокены корректны
  Vocab size: 512
  BOS=2, EOS=3, UNK=1, PAD=0

  📊 MaxSeqLen из корпуса: 64 (строка 55)

  Tokenizer: BPETokenizer (vocab=512)
  Embedding: 64, Hidden: 128
  Heads: 2, Layers: 1
  MaxSeqLen: 64
  Параметров: 103 744

  Параметры модели: 103 744
  Архитектура: Embedding → PositionalEmbedding → TransformerBlockPreNorm → LayerNorm → Linear

Обучить модель сейчас? (y/n, по умолчанию y):

╔══════════════════════════════════════╗
║   ФАЗА 1: ПРЕДВАРИТЕЛЬНОЕ ОБУЧЕНИЕ   ║
╚══════════════════════════════════════╝
Примеров: 22

  Количество эпох pretrain (по умолчанию 30): 100
  Learning rate pretrain (по умолчанию 0,001):
Валидных примеров: 22
  Warmup steps: 200
  Total steps: 2200
  Base LR: 0,001

  [Pretrain] Epoch   0/100: Loss = 7,2253, LR = 1,10E-004
  [Pretrain] Epoch   5/100: Loss = 5,0649, LR = 6,60E-004
  [Pretrain] Epoch  10/100: Loss = 4,3860, LR = 9,99E-004
  [Pretrain] Epoch  15/100: Loss = 4,0299, LR = 9,86E-004
  [Pretrain] Epoch  20/100: Loss = 3,2411, LR = 9,58E-004
  [Pretrain] Epoch  25/100: Loss = 2,3507, LR = 9,17E-004
  [Pretrain] Epoch  30/100: Loss = 1,6478, LR = 8,63E-004
  [Pretrain] Epoch  35/100: Loss = 1,1448, LR = 7,99E-004
  [Pretrain] Epoch  40/100: Loss = 0,7596, LR = 7,26E-004
  [Pretrain] Epoch  45/100: Loss = 0,5265, LR = 6,46E-004
  [Pretrain] Epoch  50/100: Loss = 0,3818, LR = 5,61E-004
  [Pretrain] Epoch  55/100: Loss = 0,3068, LR = 4,75E-004
  [Pretrain] Epoch  60/100: Loss = 0,2736, LR = 3,89E-004
  [Pretrain] Epoch  65/100: Loss = 0,2526, LR = 3,07E-004
  [Pretrain] Epoch  70/100: Loss = 0,2375, LR = 2,31E-004
  [Pretrain] Epoch  75/100: Loss = 0,2278, LR = 1,62E-004
  [Pretrain] Epoch  80/100: Loss = 0,2196, LR = 1,04E-004
  [Pretrain] Epoch  85/100: Loss = 0,2157, LR = 5,74E-005
  [Pretrain] Epoch  90/100: Loss = 0,2135, LR = 2,40E-005
  [Pretrain] Epoch  95/100: Loss = 0,2122, LR = 4,77E-006
  [Pretrain] Epoch  99/100: Loss = 0,2120, LR = 0,00E+000

╔══════════════════════════════════════╗
║   ФАЗА 2: INSTRUCTION TUNING         ║
╚══════════════════════════════════════╝
Примеров: 52

  Количество эпох finetune (по умолчанию 10): 100
  Learning rate finetune (по умолчанию 0,0001): 0.0005
Валидных примеров: 51
  Warmup steps: 200
  Total steps: 5100
  Base LR: 0,0005

  [Finetune] Epoch   0/100: Loss = 8,4320, LR = 1,28E-004
  [Finetune] Epoch   5/100: Loss = 4,5529, LR = 4,99E-004
  [Finetune] Epoch  10/100: Loss = 3,9726, LR = 4,93E-004
  [Finetune] Epoch  15/100: Loss = 3,1391, LR = 4,81E-004
  [Finetune] Epoch  20/100: Loss = 2,4474, LR = 4,62E-004
  [Finetune] Epoch  25/100: Loss = 1,9498, LR = 4,38E-004
  [Finetune] Epoch  30/100: Loss = 1,4889, LR = 4,08E-004
  [Finetune] Epoch  35/100: Loss = 1,2245, LR = 3,75E-004
  [Finetune] Epoch  40/100: Loss = 1,0092, LR = 3,38E-004
  [Finetune] Epoch  45/100: Loss = 0,8483, LR = 2,98E-004
  [Finetune] Epoch  50/100: Loss = 0,7167, LR = 2,58E-004
  [Finetune] Epoch  55/100: Loss = 0,6196, LR = 2,17E-004
  [Finetune] Epoch  60/100: Loss = 0,5503, LR = 1,77E-004
  [Finetune] Epoch  65/100: Loss = 0,4963, LR = 1,39E-004
  [Finetune] Epoch  70/100: Loss = 0,4601, LR = 1,04E-004
  [Finetune] Epoch  75/100: Loss = 0,4329, LR = 7,31E-005
  [Finetune] Epoch  80/100: Loss = 0,4148, LR = 4,67E-005
  [Finetune] Epoch  85/100: Loss = 0,4032, LR = 2,57E-005
  [Finetune] Epoch  90/100: Loss = 0,3966, LR = 1,07E-005
  [Finetune] Epoch  95/100: Loss = 0,3932, LR = 2,14E-006
  [Finetune] Epoch  99/100: Loss = 0,3926, LR = 0,00E+000

╔══════════════════════════════════════╗
║          Меню действий               ║
╠══════════════════════════════════════╣
║ 1. Предсказать текст                 ║
║ 2. Обучить модель                    ║
║ 3. Сохранить модель                  ║
║ 4. Интерактивный чат                 ║
║ 5. Информация о модели               ║
║ 6. Тест токенизатора                 ║
║ 7. Экспорт в GGUF                    ║
║ 8. Выход                             ║
╚══════════════════════════════════════╝

Выберите опцию: 4

╔══════════════════════════════════════╗
║       Интерактивный чат               ║
╠══════════════════════════════════════╣
║  Команды:                            ║
║    exit     — выход                  ║
║    temp N   — установить temperature ║
║    clear    — очистить экран         ║
╚══════════════════════════════════════╝

[temp=0,7] You: привет
Assistant:  привет! рада тебя видеть. чем могу помочь?

Да, не густо, loss=0,3926. Но даже loss=0.12 для llama.cpp это так себе результат. При загрузке модели через llama-cli с флагом –verbose, нас ругают за очень плохое обучение:

print_info: file format = GGUF V3 (latest)
print_info: file type   = all F32
print_info: file size   = 0.39 MiB (32.00 BPW)
load: missing pre-tokenizer type, using: 'default'
load:
load: ************************************
load: GENERATION QUALITY WILL BE DEGRADED!
load: CONSIDER REGENERATING THE MODEL
load: ************************************

Но всё же в llama.cpp работает

Скрытый текст

Находим где хранятся наши модели в LM Studio

Скрытый текст

Создаем примерно такой путь: D:\lmstudio\models\test\virex. Сохраняем туда gguf и готово.

Инференс

Инференс

Информация и настройки модели

Информация и настройки модели

Итого

Самым сложным было экспортировать в gguf так, что бы инференс не поломался. Изначально я пытался сделать llama модель, с такой конфигурацией: EmbeddingLayer ->Transformer Blocks (RMSNorm + RoPE + SiLU gated FFN) -> RmsNormLayer -> LinearLayer, но как-то сильно застрял в отладке. Начал всё заново. Попросил бесплатный ИИ написать новый токенизатор основываясь на исходниках мелкософта: https://github.com/dotnet/machinelearning/blob/main/src/Microsoft.ML.Tokenizers/Model/BPETokenizer.cs. И оно заработало. Не сразу, не с десятого и не с двадцатого раза, но таки удалось добиться рабочей версии.

Осталась одна проблема: инференс в llama.cpp ломается если есть вопросительный или восклицательный знак в пользовательском запросе. Необходимо дообучить модель (нужен более низкий loss).

Для тех кто хочет проверить сразу:
Исходный код: https://github.com/virex-84/LLMGPT2
Релиз (отдельно model.gguf): https://github.com/virex-84/LLMGPT2/releases/tag/v2

А на сегодня всё...