Pull to refresh
32K+
245
162,5
Rating
255
Subscribers
Send message

Хм, только в чем смысл. Ведь такими действиями по сути уничтожают ru ML сцену, которая и так очень слабо представлена, ни датасетов, ни материалов, лишь одни переводы.

Тоже прислали письмо

Добрый день,

Обратили внимание на Вашу статью https://habr.com/ru/articles/1054710/, точнее, на датасет, использованный для обучения LLM, ссылка на который содержится в статье - https://huggingface.co/datasets/IlyaGusev/habr. Этот дата-сет был создан нелегально, поэтому мы уже обратились к его создателю (тоже пользователю Хабра) с требованием об удалении. Просим Вас также удалить ссылку из статьи.

C наилучшими пожеланиями, Habr Legal Team

Там без подбора, гиперпараметры в nanochat выставляются автоматически, сам Карпатый уже давно занимается оптимизацией параметров обучения, поэтому выставлены по его личному опыту, закону Шиншиллы и так далее. Только на DPO пришлось снижать, но в nanochat вообще нет DPO этапа.

Если в целом на все эксперименты, то ушел где-то месяц, я обучал не только nanochat, обучал и MoE подобные модели, и чистый 0.25B на кастомной архитектуре которую собирал из разных новых архитектур, и даже пытался обучить 7B модельку, которую с помощью многих ухищрений удалось вместить в 32Гб, но скорость обучения была 500 t/s. Выше уже писал про дообучения 1.3B претрейна, в целом dense 1.3B вполне можно дома обучать.

И кстати может качество будет получше если обучать на меньшем количестве токенов, но уже предобученную модельку? А то обучать с нуля обычно мало смысла…

Есть и такое. У меня готова, но не опубликована, статья, где я дообучал старую ruGPT3 XL 1.3B (как раз @efreelancer недавно публиковал статью, где сделал для этой модели конвертацию в современный формат и поддержку gguf). Это pretrain 2020-2021 года обученный на 80B токенов, который умеет только продолжать текст, и выглядит это часто не как ответ, а как повествование:

Создавался этот pretrain во времена, когда ещё не было понимания, как заставить хороший pretrain вести диалог в чате, следовать инструкциям. Это время за 2 года до выхода ChatGPT и первой InstructGPT. И после дообучения на SFT такая модель на 80B токенов куда богаче по возможностям, она больше знает, хорошо следует инструкциям, может работать с текстом, даже решать головоломки:

И чтобы было понятно, какой уровень получился у модели, то сравнил её с Gemma3 1B 2025 года, при чем получилось не всегда в пользу Gemma3.

Задача на внимательность, Gemma3 1B провалил:

Про квантовую причину горения Солнца, Gemma3 1B дал не правильное название явления:

Это не полный датасет хабра, в статье есть ссылка на датасет, которую выше уже продублировали. Датасет собрал @Takagi, его же датасет IlyaGusev/saiga_preferences я использовал для SFT.

Чудесный, только очень уж теоретический материал. Интересно, возможно ли продемонстрировать его на каком-нибудь сверх-примитивном примере?

С небольшой теорией по шагам и демонстрацией промежуточных этапов, когда вначале модель не могла связать ни слова:

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

habrGPT. Обучим LLM 0.5B с нуля на статьях Хабра с помощью nanochat от Карпатого. Обучение fp8 дома и сравнение с bf16

но оценить сколько это всё таки в токенах сложно из-за дополнительных полей Интересно было бы посмотреть что получится с несколькими эпохами или большим датасетом.

Это оказалось 508M токенов, совсем мало. Но всё равно обучил модель, уже по нормальному, и в этот раз результат получился получше:

habrGPT. Обучим LLM 0.5B с нуля на статьях Хабра с помощью nanochat от Карпатого. Обучение fp8 дома и сравнение с bf16

Спасибо, хотел как раз не сразу результат, а для тех кому интересна тема обучения LLM дома, чтобы было побольше материала, тупики, ошибки и гипотезы.

Чекпоинты загрузил, обновил статью. Поддержка gguf только в форках, поэтому запуск через nanochat, нужно только разобраться куда ложить файлы и как запустить.

А какие существуют llm которые влазят в 32 гб(ну или впритык в 36) и при этом могут что-то полезное?

Вот тут я показывал опыт работы с Qwen3.6-35B-A3B, включая UD-Q2_K_XL и REAP урезание до 28B. Если памяти мало, то REAP интересная технология вырезания наименее активных экспертов, которые не нужны для определённых задач, и можно создать более компактную модель. И рядом про как ускорить без потерь за счет MTP.

REAP как агент доработала код майнкрайфта в браузере, добавила новые блоки с алмазами и создала постройку
REAP как агент доработала код майнкрайфта в браузере, добавила новые блоки с алмазами и создала постройку

Проблема в ассиметричных k и v, не все модели поддерживают такую работу на GPU и вычисления переносятся на CPU.

А симметричное q4_0 совсем плохо работает? Ключевая идея турбокванта это поворот Адамара, этот поворот добавили в llama.cpp и он по умолчанию работает для всего квантования KV, что должно давать не плохое качество на q4_0, но на практике не понятно, так это или нет.

вместо cpu-moe использовал n-cpu-moe - раскидал сколько смог слоев в видеокарту. n-cpu-moe на обычной llama.cpp дал 50 т/с, на ik_llama.cpp получил 60 т/с

Для qwen3.6 на днях добавили поддержку MTP, можно получить еще больше скорости без потери качества. Для Qwen3.6-35B-A3B ускорение не такое большое, как для Qwen3.6 27B, но оно тоже есть.

Вот тут подробнее: Qwen3.6 27B MTP весит на +0.3 Гб больше, а даёт ускорение в ~2 раза. С 60 t/s до 130 t/s без потерь. Что такое MTP

Если согласны потерять сколько-то в качестве, то можно ускориться раза в 1.5. Попробуйте кванты, которые хорошо работают на CPU, это Q4_0 и Q4_1.

Интересная идея и реализация, а есть какой-то результат для примера?

Неделю назад обучал 0.25B LLM с нуля исключительно на статьях с хабра, датасет IlyaGusev/habr, просто посмотреть, что из этого получится. SFT датасет автоматически построен из датасета хабра, чтобы не было примесей из других источников. Токенизатор обучен тоже только на хабре.

Идея была попробовать обучить 1B, но времени было мало, поэтому на коленке обучил только 0.25B за 3 часа. Первый шаг, это pre-train, который умеет только продолжать то, что ему задать как промпт:

Правильное понимание, где римские цифры, а где буквенное перечисление
Правильное понимание, где римские цифры, а где буквенное перечисление
Что-то похожее на связный текст
Что-то похожее на связный текст

Обучение pre-train было всего на 1 эпохе, поэтому знания плохо усвоены, но что-то аппроксимировано. Модель сама выявила паттерны русского языка, родов, склонений и прочего, хотя логика и знания хромают, общее написание фраз вполне корректное.

Для примера как выглядело начало обучения:

10% от 1 эпохи
10% от 1 эпохи

Дальше идет обучение SFT, где pre-train учится шаблону чата и умению отвечать на вопросы и уметь разделять где assistant, а где user. Качество общения зависит от качества SFT датасета, его я сделал автоматически и всего на 1к записей, как старт сойдет, но о проработке речи не идет:

Первое, что ответила модель после SFT обучения, все совпадения случайны
Первое, что ответила модель после SFT обучения, все совпадения случайны
0.25B LLM плохо обученная на статьях с хабра, SFT
0.25B LLM плохо обученная на статьях с хабра, SFT

Выставил температуру минимально, результаты должны быть максимально близки. Есть шанс заруинить. Допустим взяли черновую очень маленькую 0.6B

Отвечаю сам себе и уточняю для тех, кто сюда забредёт. Это информация устарела, она больше не актуальна. Проблем с черновыми моделями нет.

Спекулятивное декодирование дает идентичный результат без искажений, я специально проверил исходники llama.cpp, чтобы убедиться, что это действительно так. Особенно это актуально для новых методов MTP, Eagle-3, DFlash, которые дают сильно большее ускорение, чем старый метод через маленькие черновые модели.

Подробнее я расписал в статье: Qwen3.6 27B MTP весит на +0.3 Гб больше, а даёт ускорение в ~2 раза. С 60 t/s до 130 t/s без потерь. Что такое MTP

Так не получится сравнить, сделав несколько прогонов, вы оцениваете сэмплинг, а не модель MTP. Модель детерминирована, а сэмплинг стохастичен и сэмплинг это внешняя от модели сущность. Для сравнения надо либо отключить сэмплинг, либо собрать статистику на большом количестве замеров.

Прогнал 100 замеров без MTP и 100 с MTP на вашем задании. Температура 0.8, top_p 0.85.

Вариант с MTP выдал 64% правильных ответов, а без MTP - 57%. Но не стоит цепляться за сами проценты, в следующем прогоне будет наоборот, серия “доехать” может составлять 10 подряд, а потом 5 подряд будет “прогуляться”, один “доехать” и снова 5 “прогуляться”. Главное тут то, что температура 0.8 добавляет слишком много случайности, а top_p 0.85 креативности (значение по умолчанию в 0.95 добавляет ещё больше креативности).

Температура 0.3, процент правильных ответов стремится к 75%:

Температура 0, процент правильных ответов 100%:

Сэмплинг в данном случае вносит слишком много хаоса, модель хоть и может найти правильный ответ на задачу, но внешняя случайность от сэмплинга уводит модель в сторону. MTP тут не при чём, в статье я как раз смотрел исходники llama.cpp чтобы убедиться, что MTP не добавляет от себя ничего и никак не искажает генерацию.

Код скрипта, для тех кто хочет повторить
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Тест-прогоны llama.cpp (v1 + stream)</title>
<style>
  * { box-sizing: border-box; }
  body { font-family: system-ui, sans-serif; margin:0; background:#1e1e2e; color:#cdd6f4; height:100vh; overflow:hidden; }

  /* ===== Scrollbars ===== */
  ::-webkit-scrollbar { width: 10px; height: 10px; }
  ::-webkit-scrollbar-track { background: #181825; border-radius: 6px; }
  ::-webkit-scrollbar-thumb {
    background: #45475a;
    border-radius: 6px;
    border: 2px solid #181825;
    transition: background .2s;
  }
  ::-webkit-scrollbar-thumb:hover { background: #89b4fa; }
  ::-webkit-scrollbar-corner { background: #181825; }

  /* Firefox */
  * {
    scrollbar-width: thin;
    scrollbar-color: #45475a #181825;
  }

  .layout { display:flex; height:100vh; }
  .main { flex:1; overflow-y:auto; padding:20px; min-width:0; }
  .side { width:480px; border-left:1px solid #45475a; display:flex; flex-direction:column; background:#181825; }
  .side-header { padding:10px 14px; background:#313244; font-weight:600; border-bottom:1px solid #45475a; }
  .side-body { flex:1; overflow-y:auto; padding:12px 14px; font-family: monospace; font-size:12.5px; white-space:pre-wrap; word-break:break-word; line-height:1.5; }
  .side-body .thinking { color:#9399b2; font-style:italic; }
  .side-body .content { color:#cdd6f4; }
  .side-body .sep { color:#89b4fa; margin:8px 0; display:block; border-top:1px dashed #45475a; padding-top:8px; }
  h1 { color:#89b4fa; margin-top:0; }
  .card { background:#313244; padding:16px; border-radius:10px; margin-bottom:16px; }
  label { display:block; margin:8px 0 4px; font-size:13px; color:#bac2de; }
  input, button, textarea { padding:8px 12px; font-size:14px; border-radius:6px; border:1px solid #45475a; background:#1e1e2e; color:#cdd6f4; }
  button { background:#89b4fa; color:#1e1e2e; font-weight:600; cursor:pointer; border:none; margin-right:8px; }
  button:disabled { background:#585b70; cursor:not-allowed; color:#a6adc8; }
  .log { font-family: monospace; font-size:12px; background:#11111b; padding:10px; max-height:320px; overflow-y:auto; border-radius:6px; }
  .log div { margin-bottom:4px; border-bottom:1px solid #313244; padding-bottom:4px; }
  .ok { color:#a6e3a1; } .fail { color:#f38ba8; } .warn { color:#f9e2af; }
  table { width:100%; border-collapse:collapse; margin-top:10px; }
  th, td { padding:6px 10px; border:1px solid #45475a; text-align:left; }
  th { background:#45475a; }
  .progress { height:8px; background:#45475a; border-radius:4px; overflow:hidden; margin:10px 0; }
  .progress > div { height:100%; background:#a6e3a1; transition: width .2s; }
  textarea { width:100%; }
  .hint { font-size:12px; color:#9399b2; margin-top:4px; }
  .sampling-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:10px; margin-top:10px; }
  .sampling-grid label { margin:0 0 4px; font-size:12px; }
  .sampling-grid input { width:100%; }
  .btn-stop { background:#f38ba8 !important; }
</style>
</head>
<body>
<div class="layout">
  <div class="main">
    <h1>🧪 Прогоны llama.cpp (v1 / streaming)</h1>

    <div class="card">
      <label>URL сервера (v1 endpoint):</label>
      <input id="url" value="http://localhost:8080/v1/chat/completions" style="width:90%">
      <div class="hint">Используется OpenAI-совместимый эндпоинт. По умолчанию <code>/v1/chat/completions</code>.</div>

      <label>Количество прогонов в серии:</label>
      <input id="count" type="number" value="25" min="1" max="500">

      <label>Параметры sampling:</label>
      <div class="sampling-grid">
        <div>
          <label>temperature</label>
          <input id="temperature" type="number" value="0.8" step="0.1" min="0">
        </div>
        <div>
          <label>top_p</label>
          <input id="top_p" type="number" value="0.85" step="0.05" min="0" max="1">
        </div>
        <div>
          <label>top_k</label>
          <input id="top_k" type="number" value="40" step="1" min="0">
        </div>
        <div>
          <label>min_p</label>
          <input id="min_p" type="number" value="0.05" step="0.01" min="0" max="1">
        </div>
        <div>
          <label>presence_penalty</label>
          <input id="presence_penalty" type="number" value="0.0" step="0.1" min="-2" max="2">
        </div>
        <div>
          <label>frequency_penalty</label>
          <input id="frequency_penalty" type="number" value="0.0" step="0.1" min="-2" max="2">
        </div>
      </div>

      <label style="margin-top:16px;">Промпт (user message):</label>
      <textarea id="prompt" rows="6" style="background:#11111b;padding:8px;border-radius:6px;border:1px solid #45475a;">Вопрос: "Моя машина грязная и я хочу её помыть. Автомойка в 100 м от дома. Как мне поступить: доехать на машине или прогуляться пешком? Ответь одним словом: 'доехать' или 'прогуляться'." Дай ответ на этот вопрос и оценку точности ответа, где 1.0 - абсолютно точно, 0.0 - абсолютно не точно и это критерий оценки твоей уверенности в этом ответе. Значения могут лежать в диапазоне от 0.0 до 1.0. Выдача должна быть в JSON со следующими полями: answer, accuracy</textarea>

      <div style="margin-top:12px;">
        <button id="btn1">▶ Начать прогон "Без MTP"</button>
        <button id="btn2">▶ Начать прогон "С MTP"</button>
        <button id="btnStop" class="btn-stop" style="display:none;">⏹ Остановить</button>
        <button id="btnReset">🔄 Сброс</button>
        <button id="btnClearStream" style="background:#f38ba8;color:#1e1e2e;">🧹 Очистить панель</button>
      </div>
      <div class="progress"><div id="bar" style="width:0%"></div></div>
      <div id="status" style="margin-top:6px;"></div>
    </div>

    <div class="card">
      <h3>📊 Статистика</h3>
      <table id="stats">
        <tr><th>Метрика</th><th>Без MTP</th><th>С MTP</th></tr>
        <tr><td>Всего запросов</td><td id="s1_total">—</td><td id="s2_total">—</td></tr>
        <tr><td>Валидный JSON</td><td id="s1_valid">—</td><td id="s2_valid">—</td></tr>
        <tr><td>Ответ "доехать"</td><td id="s1_drive">—</td><td id="s2_drive">—</td></tr>
        <tr><td>Ответ "прогуляться"</td><td id="s1_walk">—</td><td id="s2_walk">—</td></tr>
        <tr><td>Правильных ответов (%)</td><td id="s1_correct_pct">—</td><td id="s2_correct_pct">—</td></tr>
        <tr><td>Ошибок JSON</td><td id="s1_err">—</td><td id="s2_err">—</td></tr>
      </table>
    </div>

    <div class="card">
      <h3>📝 Лог</h3>
      <div id="log" class="log"></div>
    </div>
  </div>

  <div class="side">
    <div class="side-header">💬 Поток ответа модели</div>
    <div id="stream" class="side-body"><span style="color:#6c7086;">Здесь в реальном времени появится генерация…</span></div>
  </div>
</div>

<script>
const logEl = document.getElementById('log');
const statusEl = document.getElementById('status');
const bar = document.getElementById('bar');
const streamEl = document.getElementById('stream');
const btn1 = document.getElementById('btn1');
const btn2 = document.getElementById('btn2');
const btnStop = document.getElementById('btnStop');
const btnReset = document.getElementById('btnReset');
const btnClearStream = document.getElementById('btnClearStream');

let isRunning = false;
let currentController = null;

let stats1 = { total: 0, valid: 0, drive: 0, walk: 0, accSum: 0, accCount: 0, latSum: 0, err: 0 };
let stats2 = { total: 0, valid: 0, drive: 0, walk: 0, accSum: 0, accCount: 0, latSum: 0, err: 0 };

function addLog(msg, cls='') {
  const d = document.createElement('div');
  d.className = cls;
  d.textContent = msg;
  logEl.prepend(d);
}
function setStatus(t) { statusEl.textContent = t; }
function setProgress(p) { bar.style.width = p + '%'; }

function streamStart(reqIndex) {
  const sep = document.createElement('span');
  sep.className = 'sep';
  sep.textContent = `── запрос #${reqIndex} ──`;
  streamEl.appendChild(sep);
  streamEl.scrollTop = streamEl.scrollHeight;
}
function streamChunk(text, isThinking) {
  const span = document.createElement('span');
  span.className = isThinking ? 'thinking' : 'content';
  span.textContent = text;
  streamEl.appendChild(span);
  streamEl.scrollTop = streamEl.scrollHeight;
}
function streamEnd() {
  const br = document.createElement('br');
  streamEl.appendChild(br);
}

function extractJson(raw) {
  const withoutThink = raw.replace(/<think\b[\s\S]*?<\/think>/gi, '');
  const matches = [...withoutThink.matchAll(/\{[\s\S]*?\}/g)];
  if (matches.length === 0) {
    const fallback = [...raw.matchAll(/\{[\s\S]*?\}/g)];
    if (fallback.length === 0) throw new Error('JSON не найден');
    return JSON.parse(fallback[fallback.length - 1][0]);
  }
  return JSON.parse(matches[matches.length - 1][0]);
}

function validateResponse(obj) {
  const issues = [];
  if (typeof obj !== 'object' || obj === null) issues.push('не объект');
  if (!('answer' in obj)) issues.push('нет поля answer');
  if (!('accuracy' in obj)) issues.push('нет поля accuracy');
  const a = typeof obj.answer === 'string' ? obj.answer.trim().toLowerCase() : '';
  if (a !== 'доехать' && a !== 'прогуляться') issues.push(`answer="${obj.answer}" (ожидалось "доехать"/"прогуляться")`);
  const acc = Number(obj.accuracy);
  if (isNaN(acc) || acc < 0 || acc > 1) issues.push(`accuracy=${obj.accuracy} (вне [0..1])`);
  return { ok: issues.length === 0, issues, answer: a, accuracy: isNaN(acc) ? null : acc };
}

function getSamplingParams() {
  const params = {};
  const temperature = parseFloat(document.getElementById('temperature').value);
  const top_p = parseFloat(document.getElementById('top_p').value);
  const top_k = parseInt(document.getElementById('top_k').value);
  const min_p = parseFloat(document.getElementById('min_p').value);
  const presence_penalty = parseFloat(document.getElementById('presence_penalty').value);
  const frequency_penalty = parseFloat(document.getElementById('frequency_penalty').value);

  if (!isNaN(temperature)) params.temperature = temperature;
  if (!isNaN(top_p) && top_p >= 0 && top_p <= 1) params.top_p = top_p;
  if (!isNaN(top_k) && top_k >= 0) params.top_k = top_k;
  if (!isNaN(min_p) && min_p >= 0 && min_p <= 1) params.min_p = min_p;
  if (!isNaN(presence_penalty)) params.presence_penalty = presence_penalty;
  if (!isNaN(frequency_penalty)) params.frequency_penalty = frequency_penalty;

  return params;
}

function updateTable() {
  document.getElementById('s1_total').textContent = stats1.total || '—';
  document.getElementById('s1_valid').textContent = stats1.valid || '—';
  document.getElementById('s1_drive').textContent = stats1.drive || '—';
  document.getElementById('s1_walk').textContent = stats1.walk || '—';
  document.getElementById('s1_correct_pct').textContent = stats1.total ? ((stats1.drive / stats1.total * 100).toFixed(1) + '%') : '—';
  document.getElementById('s1_err').textContent = stats1.err || '—';

  document.getElementById('s2_total').textContent = stats2.total || '—';
  document.getElementById('s2_valid').textContent = stats2.valid || '—';
  document.getElementById('s2_drive').textContent = stats2.drive || '—';
  document.getElementById('s2_walk').textContent = stats2.walk || '—';
  document.getElementById('s2_correct_pct').textContent = stats2.total ? ((stats2.drive / stats2.total * 100).toFixed(1) + '%') : '—';
  document.getElementById('s2_err').textContent = stats2.err || '—';
}

function resetStats(num) {
  if (num === 1) {
    stats1 = { total: 0, valid: 0, drive: 0, walk: 0, accSum: 0, accCount: 0, latSum: 0, err: 0 };
  } else {
    stats2 = { total: 0, valid: 0, drive: 0, walk: 0, accSum: 0, accCount: 0, latSum: 0, err: 0 };
  }
  updateTable();
}

async function streamCompletion(url, prompt, reqIndex, signal) {
  const t0 = performance.now();
  const samplingParams = getSamplingParams();
  
  const body = {
    model: "local-model",
    messages: [{ role: "user", content: prompt }],
    stream: true,
    ...samplingParams
  };

  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
    signal
  });
  if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + (await res.text()).slice(0, 300));

  const reader = res.body.getReader();
  const decoder = new TextDecoder('utf-8');
  let buffer = '';
  let fullText = '';
  let inThink = false;

  streamStart(reqIndex);

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    let idx;
    while ((idx = buffer.indexOf('\n\n')) !== -1) {
      const event = buffer.slice(0, idx);
      buffer = buffer.slice(idx + 2);
      const lines = event.split('\n');
      for (const line of lines) {
        if (!line.startsWith('data:')) continue;
        const payload = line.slice(5).trim();
        if (payload === '[DONE]') continue;
        if (!payload) continue;
        try {
          const j = JSON.parse(payload);
          const delta = j.choices?.[0]?.delta?.content ?? '';
          if (delta) {
            fullText += delta;
            const chunkForCheck = fullText.slice(-Math.max(delta.length + 10, 20));
            if (!inThink && /<think\b/i.test(chunkForCheck)) inThink = true;
            if (inThink && /<\/think\s*>/i.test(chunkForCheck)) {
              streamChunk(delta, true);
              inThink = false;
              continue;
            }
            streamChunk(delta, inThink);
          }
        } catch (e) { /* skip */ }
      }
    }
  }
  streamEnd();
  return { content: fullText, latency: performance.now() - t0 };
}

async function runSeries(seriesNum, seriesName) {
  if (isRunning) return;

  const url = document.getElementById('url').value.trim();
  const count = parseInt(document.getElementById('count').value, 10);
  const prompt = document.getElementById('prompt').value;
  const samplingParams = getSamplingParams();

  resetStats(seriesNum);
  const stats = seriesNum === 1 ? stats1 : stats2;

  isRunning = true;
  currentController = new AbortController();
  const signal = currentController.signal;

  btn1.disabled = true;
  btn2.disabled = true;
  btnReset.disabled = true;
  btnStop.style.display = 'inline-block';

  addLog(`=== Запуск прогона "${seriesName}" (${count} запросов) ===`, 'ok');
  addLog(`Параметры: ${JSON.stringify(samplingParams)}`, 'ok');
  setStatus(`Прогон "${seriesName}": 0 / ${count}`);

  try {
    for (let i = 1; i <= count; i++) {
      if (signal.aborted) break;

      stats.total++;
      try {
        const { content, latency } = await streamCompletion(url, prompt, i, signal);
        stats.latSum += latency;

        let obj;
        try {
          obj = extractJson(content);
        } catch (e) {
          stats.err++;
          addLog(`#${i} ❌ JSON parse: ${e.message} | raw tail: …${content.slice(-120)}`, 'fail');
          updateTable();
          continue;
        }

        const v = validateResponse(obj);
        if (v.ok) {
          stats.valid++;
          if (v.answer === 'доехать') stats.drive++;
          else if (v.answer === 'прогуляться') stats.walk++;
          if (v.accuracy !== null) { stats.accSum += v.accuracy; stats.accCount++; }
          addLog(`#${i} ✅ answer="${v.answer}" accuracy=${v.accuracy} latency=${latency.toFixed(0)}ms`, 'ok');
        } else {
          stats.err++;
          addLog(`#${i} ⚠ schema: ${v.issues.join(', ')} | raw: ${content.slice(-140)}`, 'warn');
        }
      } catch (e) {
        if (e.name === 'AbortError') {
          addLog(`⏹ Прогон остановлен пользователем`, 'warn');
          break;
        }
        stats.err++;
        addLog(`#${i} 🚫 request: ${e.message}`, 'fail');
      }

      updateTable();
      setStatus(`Прогон "${seriesName}": ${i} / ${count}`);
      setProgress(i / count * 100);
    }

    addLog(`=== Прогон "${seriesName}" завершён: валидных ${stats.valid}/${stats.total} ===`, 'ok');
  } catch (e) {
    addLog(`🚫 Критическая ошибка: ${e.message}`, 'fail');
  } finally {
    isRunning = false;
    currentController = null;
    btn1.disabled = false;
    btn2.disabled = false;
    btnReset.disabled = false;
    btnStop.style.display = 'none';
    if (!signal.aborted) {
      setStatus('');
    }
  }
}

btn1.onclick = () => runSeries(1, 'Без MTP');
btn2.onclick = () => runSeries(2, 'С MTP');

btnStop.onclick = () => {
  if (currentController) {
    currentController.abort();
    setStatus('Остановка...');
  }
};

btnReset.onclick = () => {
  logEl.innerHTML = '';
  setProgress(0);
  setStatus('');
  resetStats(1);
  resetStats(2);
};

btnClearStream.onclick = () => {
  streamEl.innerHTML = '';
};
</script>
</body>
</html>

Скачать кванты с именем MTP:

И запускать:

.\llama-server -m "Qwen3.6-27B-UD-Q4_K_XL.gguf" --spec-type draft-mtp --spec-draft-n-max 4

Указать --spec-draft-n-max обязательно, по умолчанию будет сильная просадка скорости.

Ещё из новостей про llama.cpp:

До сих пор не добавили TurboQuant, но на то есть причина - ощутимая просадка скорости, так как вычисления до сих пор не смогли полноценно перенести на GPU и работа выполняется на CPU. Вместо этого добавили “у нас есть turboquant дома”, а именно Rotate Activations. Это одна из частей турбокванта, вращение активаций через матрицу Адамара. И даже это очень сильно повысило качество квантования KV-кэша в размере Q4_0.

Это улучшение включено по умолчанию, и работает для KV-кэша всех вариантов (q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1). Я прогнал пару тестов Q4_0 на 256к контексте, и сработало не плохо, не хуже чем Q8_0.

Добавили нативную поддержку NVFP4 в GGUF. NVFP4 лучше снижает ошибку квантования чем MXFP4, так как разбивает микроблоки дополнительно в 2 раза, делая их более гранулированными, и используют адаптивный scale factor для этих блоков, повышая точность. Я прогнал пару замеров KLD, и кванты NVFP4 GGUF и правда лучше чем чистые MXFP4.

В llama.cpp завезли поддержку Qwen3.6 MTP. Новые кванты уже создали со слоями MTP. Написал статью что такое MTP, как запустить и какое ускорение получилось. Также проверил исходники llama.cpp, чтобы проверить, качество оригинала сохраняется или искажается:

Qwen3.6 27B MTP весит на +0.3 Гб больше, а даёт ускорение в ~2 раза. С 60 t/s до 130 t/s без потерь. Что такое MTP

Еще можно какие-нибудь Q2_K_L от bartowski попробовать - с гарантированно равномерным квантованием экспертов.

Это хороший аргумент, но есть несколько нюансов:

  1. bartowski статические кванты квантует с использованием imatrix, поэтому равномерного квантования там не может быть, неравномерность задается на уровне суперблоков через imatrix.

  2. У него калибровачный датасет 400к токенов, из них только 4к на русском, 1%. И этот 1% не отборных текстов, а каких-то разорванных кусков.

    calibration_datav5.txt от Bartowski, отсортирован только русский текст
    calibration_datav5.txt от Bartowski, отсортирован только русский текст
  3. bartowski тоже не квантует равномерно, часть слоев выше, часть ниже.

    Bartowski, qwen3.6-35B-A3B, Q4_K_M и Q2_K_L
    Bartowski, qwen3.6-35B-A3B, Q4_K_M и Q2_K_L

Поэтому его кванты не чистые статические классические Q4_K_M, а являются динамическими. Хотя тут, смотря что вы имели ввиду под "гарантированно равномерным".

Вообще, мне нравятся кванты от ubergarm, они весят меньше, что важно когда мало VRAM, используют новые алгоритмы квантования, которые сохраняют больше точности, квантование тоже равномерное, без сильных экспериментов, imatrix содержит уже 2% русского текста.

Но кванты ubergarm сложно рекомендовать, так как требуют понимая ik_llama.cpp, которая не имеет автонастроек fit, не дружит с AMD и тому подобное.

Ещё из интересного для квантов, недавно завезли NVFP4 GGUF в llama.cpp. NVFP4 имеет в 2 раза меньший размер микроблоков, что повышает точность суперблоков, и имеет более детальный scale factor чем у MXFP4, когда внутри в рецептах начнут использовать nvfp4 вместо Q4_K, то это, возможно, не плохо повысит качество.

Чистые NVFP4 тоже работают не плохо, уже в GGUF виде можно запускать.

Это хорошо, что он откатил свой рецепт, я перестал качать его кванты когда он начал, пытаясь выиграть в размере, часть ffn квантовать в Q3_K, что ударяло по качеству Q4_K_M. Если он ещё увеличит процент русского текста в своем imatrix с 1% хотя бы до 5%, то его кванты будут совсем хороши.

1
23 ...

Information

Rating
30-th
Registered
Activity