Тензорные ядра RTX 3060 разблокированы: прирост 83 %, но на квантованной модели он почти исчезает

Прокомментировать Просмотры: 9

23 октября 2026 года в репозиторий Zagitta/gpu-unlock приняли поддержку GA106, то есть RTX 3060. К тому времени мы уже почти сутки изучали собственную карту. Мы успели сделать замеры до и после разблокировки и получили данные, которых нет в чужих отчётах.

Итог в двух словах: производительность FP16 на матрице 8192³ выросла с 27,1 до 49,5 TFLOPS (×1,83). Контрольный FP32 не изменился даже на десятую долю. На нашей квантованной модели прирост нулевой. На квантованном стеке в целом он составляет около пяти процентов, но об этом отдельно ниже. Оба результата получены на одной карте и одним и тем же скриптом.

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

Что на самом деле ограничивают в «урезанных» картах

Когда говорят об «урезанных картах», обычно смешивают два разных явления. Это первая ловушка.

Что ограничивают

Чем

Обратимо ли

Отбраковка

количество SM, ширину шины, объём VRAM

фьюзы и лазерная подрезка на заводе

нет, это физическое ограничение

Гейтинг

скорость конвейеров

регистр-селектор, значение которого пересчитывается при каждом холодном старте

да, пока не загружен драйвер

Отбраковка честна: кристалл с двумя неисправными SM не выбрасывают, а продают как младшую модель. Гейтинг же служит маркетинговой сегментации. Один и тот же кристалл попадает в RTX 3090, в 3080 Ti и в A6000, и чтобы A6000 не покупали по цене 3090, на неё ставят замок.

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

На GA102 и GA106 за это отвечают три регистра в BAR0:

Регистр

Смещение

Назначение

FEAT_OVR_PLM

0x00823804

маска привилегий: младшие 4 бита определяют чтение по уровням, следующие 4 бита определяют запись

FEAT_OVR_SM_SPD (SS0)

0x0082381c

восемь 4-битных полей: DP, FFMA, FMLA16, FMLA32, IMLA0…3

FEAT_OVR_SM_SPD_1 (SS1)

0x00823820

единственное поле, IMLA4

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

Холодное значение FEAT_OVR_PLM одинаково у всех GA10x, для которых у нас есть логи (GA102 и GA106): 0xffffff8f. Младший байт 1000 1111 означает, что читать могут все четыре уровня, а писать — только L3. Поэтому модуль не обращается к селекторам напрямую. Сначала он повышает уровень привилегий и лишь затем записывает во все поля нужную скорость.

В каждом поле старший бит служит флагом «override включён», а три младших задают скорость (00 — полная, 01 — половина, 10 — четверть, 11 — одна восьмая; два последних значения автор репозитория не подтверждал). Отсюда вытекает деталь, которую легко понять наоборот: 7 и 8 не являются соседними ступенями одной шкалы. Значение 7 — это 0111: override выключен, и младшие биты ничего не определяют. Значение 8 — это 1000: override включён, скорость полная. Числа, различающиеся на единицу, отвечают на разные вопросы и не описывают разную скорость.

Разведка перед записью

Мы начали с чтения, и это было верное решение не из осторожности, а в силу самой структуры задачи. У инструмента есть проверки, которые срабатывают до первой записи в карту. Их можно проверить заранее тем же доступом на чтение. Если первая из них не пройдёт, писать вообще не придётся.

Первым делом модуль разбирает VBIOS самой карты по смещению 0x300000 в BAR0 и берёт оттуда четыре поля: engine_mask, ucode_id, sig_count, sig_versions. У нас есть готовый пробник только для чтения (gpu_probe.py, отдельный от модуля). Он разбирает этот заголовок и проверяет условия в том же порядке, в каком их проходит модуль.

Вот что он выдал на нашей карте:

FWSEC geometry: engine=0x400 ucode=9 signatures=3 versions=0x7
                bytes=1152 imem=0xe200 dmem=0x800
                image_off=0x416bc size=0xea00

Теперь это нужно с чем-то сопоставить. В репозитории есть интеграционный тест с числами, снятыми с реального PROM платы GA102. Оговорка, без которой эти цифры значат больше, чем должны: блок с эталоном выполняется, только если тесту передан дамп PROM (в коде это if (argc == 2), рядом стоит комментарий «Optional integration check; normal tests contain no board-owned blobs»). В обычном прогоне эти строки не исполняются. Перед нами зафиксированное ожидание из чужого дампа, а не проверка, которая сработает у любого, кто клонирует репозиторий. Тем не менее свои значения с ними сравнить можно, и это полезно.

Поле

GA102 (эталон)

Наша GA106

engine_mask

0x400

0x400

совпало

ucode_id

9

9

совпало

sig_count

3

3

совпало

sig_versions

0x7

0x7

совпало

dmem

0x800

0x800

совпало

imem

0xda00

0xe200

различается

image_off

0x4289c

0x416bc

различается

image_size

0xe200

0xea00

различается

Совпали именно те поля, на основании которых модуль принимает решения: engine_mask (единственное допустимое значение — 0x400), ucode_id (определяет выбор фьюза), sig_versions (определяет слот подписи), а также sig_count и dmem. Расходятся три поля, описывающие сам образ: размер imem, смещение образа и его размер.

Это не погрешность замера, а ответ на вопрос, почему нам не потребовалось собирать собственный payload. Модуль не сверяет эти три поля с константами, а берёт их из ROM карты (memcpy(image, rom + fw.image_off, fw.image_size), при этом fw.imem_size передаётся при запуске Falcon). Всё различающееся считывается с платы, а чужими остаются только загрузчик и подпись. Поэтому ассеты от GA102 подошли к GA106 без единой правки.

Одно наблюдение мы приводим именно как наблюдение, а не как вывод: наши значения перекликаются с чужими крест-накрест. Наш imem = 0xe200 равен чужому image_size = 0xe200. Это повод не считать наш разбор полей окончательно верным. На сделанный выше вывод это не влияет (условие допуска завязано на engine_mask, а геометрия читается с самой карты). Но тем, кто будет сверять всё построчно, лучше знать, где у нас остались сомнения.

Вторая находка пробы: engine_mask = 0x400 — единственное значение, которое модуль принимает. Всё остальное он отвергает. Окажись там другое число, эксперимент завершился бы на чтении: ноль записей и честный ответ «на этой плате так нельзя».

Все проверки были пройдены. Только после этого мы собрали модуль.

Окно возможностей и холодная карта

Схема запуска выглядит так:

питание -> ROM платы -> FWSEC -> Booter -> GSP -> драйвер
                          |         |
                          |         +-- здесь карта принимает подпись
                          +-- здесь поднимается WPR2

Модуль встраивается в тот момент, когда драйвер ещё не привязан к карте. Отсюда требование, которое легко принять за суеверие: полностью выключать питание, а не перезагружаться.

Помехой становится не сама перезагрузка, а состояние, в которое карту привёл драйвер. Писать можно только в том цикле питания, в котором драйвер ещё ни разу не поднимал WPR2. Модуль сообщает об этом прямо: card already initialised since power on (WPR2 enabled); power off first. Тёплая перезагрузка в Linux селекторы не сбрасывает (это следует из чужого отчёта, о нём в разделе про отрицательный результат), но окно заново не открывает. Оно закрылось при первой загрузке драйвера, а не при выключении.

Для проверки мы сделали live-USB с Ubuntu, где nouveau занесён в чёрный список, а драйвер NVIDIA вообще не загружается. Благодаря этому карта остаётся нетронутой вплоть до момента запуска.

Три полезных подводных камня, каждый из которых стоил нам времени:

Первый. Каталог /media/ubuntu появляется лишь после монтирования флешки. Загрузочная флешка Ubuntu монтируется в /media/cdrom и только на чтение. Логи, которые пишутся «рядом со скриптом», при зависании пропадают. Решение: fsync после каждой строки и вторая флешка для записи.

Второй. Команда sudo -i открывает логин-шелл в домашнем каталоге root и не наследует текущую папку. Связка cd /media/... && sudo -i приведёт вас в /root, а не туда, куда вы рассчитывали.

Третий. Перед insmod нужно убедиться, что карту не занял драйвер. Правильная команда — lspci -nnk -s 01:00.0, а не -d 10de:01:00.0. Второй вариант — это фильтр «вендор:устройство», он вернёт пустой вывод, а пустоту легко истолковать как «драйвер не привязан».

Замеры

Скрипт замера использовался один и тот же: файл копировался на флешку и запускался и в Linux, и в Windows. Хеш зафиксирован в манифесте для Linux-прогона, а для Windows-копии мы сверку по хешу не выполняли. Это не мелочь, а существенная оговорка: без неё «тот же скрипт» остаётся словами, а не фактом.

Протокол: GEMM 4096³ и 8192³, плотная матрица, накопление в FP32, медиана из 20 прогонов после 3 прогревочных, CuPy 14.2.0 с cuBLAS из wheel.

Тест

До

После

fp16 8192³

27,1

49,5

×1,83

fp16 4096³

23,9

46,5

×1,95

fp32 8192³ (контроль)

9,9

9,9

×1,00

fp32 4096³ (контроль)

7,8

7,8

×1,00

Отношение fp16/fp32: 2,74 → 5,00.

Контроль не сдвинулся даже на десятую. Это не декорация: без неподвижного контроля фраза «стало быстрее» может означать что угодно, в том числе то, что вы измерили не тот конвейер.

Питание и частота, выборка каждые 200 мс:

До

После

частота ядра

не сэмплировалась

1747–1777 МГц под нагрузкой (12 сэмплов от 150 Вт, медиана 1762); выше 1900 только при недогрузке

пик мощности

170 Вт, то есть упёрлась в лимит

170,7 Вт при лимите 170

Лимит питания нашей платы зашит в VBIOS: Max Power Limit = Default = 170. Через nvidia-smi его не поднять, поэтому по мощности карта упирается в потолок в обеих сессиях.

Частота «до» отсутствует в таблице не случайно: в первой сессии nvidia-smi dmon записывался без колонки частот. Поэтому 1891 МГц для «до» — это вывод из 27,1 TFLOPS, а не измерение, и сопоставлять его с сэмплом нельзя. Зато сэмпл «после» даёт независимую проверку главного утверждения. Если по 49,5 TFLOPS и частоте под нагрузкой (1747–1777 МГц) рассчитать, сколько операций на SM за такт выполняет карта, получится 995–1012 против ожидаемых 1024. Удвоение видно не только в TFLOPS: оно согласуется с частотой, которую измерял отдельный прибор, а не тот же самый скрипт.

Как убедиться, что прибор измеряет именно то, что нужно

Самым полезным приёмом за все двое суток оказался бесплатный: предсказать базовую линию из первых принципов.

У GA106 28 SM, 512 флоп на SM за такт для FP16 с накоплением FP32 (это как раз та половина, которую ограничивали), частота буста около 1,89 ГГц.

28 × 512 × 1,89 = 27,2 TFLOPS   (предсказание)
замер            = 27,1 TFLOPS

Значения совпали. Это значит, что cuBLAS выбрал именно тот путь, который и удваивается, а не какой-то иной. Тот же приём можно проверить и на чужой карте: 84 SM × 512 × 1,384 ГГц = 59,5 против 59,6, измеренных на 3090 Ti в чужом отчёте.

Без этого шага «27,1» — просто число. С ним это уже утверждение о том, что именно вы измеряете.

Чего это ускорение не даёт

Теперь начинается неприятная часть, ради которой и стоило писать эту статью.

В том же PR, на такой же карте, автор запустил llama.cpp с квантованной моделью:

gemma-4-12B Q4_K_XL, RTX 3060:
  pp512   1460,5 -> 1467,1 tok/s   (+0,5 %, шум)
  tg128     43,63 -> 43,65 tok/s   (+0,05 %, шум)

На этой модели и в этом профиле прирост нулевой. Квантованный путь в llama.cpp проходит не через тот конвейер, который переключает unlock.

Что именно там происходит, этот замер не показывает: он лишь говорит, что выигрыша нет. INT8 на Ampere — отдельный путь со своими полями в селекторах (IMLA0…4), и его пиковую производительность мы не измеряли. Подтверждённого замера, который показал бы, какое именно ядро выполняет конкретный тип квантованного матмула в ggml на sm_86, у нас нет, и подменять его догадкой мы не станем.

Второе ограничение, не связанное с квантами: декодирование упирается в пропускную способность памяти. У 3060 это 360 ГБ/с на 192-битной GDDR6. Селектор регулирует скорость конвейеров, а не памяти, поэтому на пошаговую генерацию токенов unlock не влияет вовсе.

Мы рассчитывали, что prefill (обработка промпта) даст выигрыш, и отчасти это верно: по логам нашего движка на 37 запросах подряд prefill занимает 26 % времени, а при промахе кэша промпта доля доходит до 51 % и 95 %. Но 26 % времени не превращаются в 13 % ускорения, если внутри этих 26 % работают не те ядра.

Где ноль перестаёт быть нулём. Ноль справедлив для квантованного пути и для декодирования, но универсальным он не является: прирост равен доле математики bf16/fp16/tf32 в нагрузке. Это видно по чужим замерам, опубликованным alesha pro (числа его, мы их не перепроверяли): bf16 на тензорных ядрах 82 → 155 TFLOPS (×1,89), cuBLAS BF16 GEMM 8192³ 64 → 99, TF32 41 → 79 (×1,93). На реальной нагрузке, Strata с Qwen3.8 Flash-Next на двух картах, prefill дал +12 % на 4K и 10K и +10 % на 90K (3205 → 3536 т/с). Автор при этом прямо оговаривает, что для этого движка случай почти худший: основная часть prefill там приходится на int8 и копирования.

Есть и ещё одна точка, которая уточняет нашу прежнюю формулировку. В том же чужом отчёте есть замер на int8-стеке: vLLM с Qwen 27B (W4A8 Marlin). Профилирование показало около 29 % времени GPU в BF16/FP16-ядрах с накоплением FP32, а сквозной выигрыш составил около 5 %, на грани шума между прогонами. Иначе говоря, на квантованном стеке выигрыш не нулевой, а небольшой, потому что таких ядер там меньше трети. Это же объясняет, почему формулировка «ноль для квантованного инференса» шире замера. Верная звучит так: ноль для нашего профиля.

Итого на одной кривой шесть точек: наш ноль на Gemma Q4 в llama.cpp, наш ×1,83 на чистом FP16 GEMM, чужие ×1,89 и ×1,93 на 3090, чужие +10–12 % на живом движке и чужие +5 % на стеке, который уже опирается на int8. Наш ноль не опровергает кривую, а лежит на одном из её концов, и утверждать по нему, что «и всё остальное тоже ноль», было бы неправдой.

Отрицательный результат, который стоит зафиксировать

Мы проверили, переживут ли селекторы переход в Windows.

План был таким: разблокировать карту в Linux, не отключая питание, выполнить reboot, выбрать Windows и прогнать тот же скрипт. То, что питание между сессиями действительно не отключалось, мы тогда не зафиксировали, и это оказалось главной дырой в тесте.

Windows 10, CuPy 14.2.0, CUDA API 13.40, тот же gemm_bench.py:
  fp16 8192³ = 27,1 и 27,0 TFLOPS
  fp32 8192³ = 9,9 и 9,9

Это в точности сток. При этом по чужому отчёту тёплая перезагрузка в Linux селекторы сохраняет: там отношение TFLOPS на мегагерц после ребута не изменилось, то есть сам по себе ребут их не стирает.

Различающий тест короток, но мы его не провели: разблокировать в Linux, прочитать FEAT_OVR_SM_SPD, выполнить reboot (не poweroff), сразу загрузить Windows и прогнать тот же скрипт. Результат около 43–48 TFLOPS означал бы, что селекторы живы и обход до загрузки ОС не нужен. Результат около 27 при подтверждённой непрерывности питания означал бы, что их сбрасывает инициализация драйвера.

Практическая рекомендация на сегодня: либо отдельный диск с Linux, либо подход, работающий до загрузки ОС. Второй вариант уже реализован другими: есть UEFI-решение для Windows, которое стартует до bootmgr, разблокирует карту и передаёт управление дальше. Автор проверил его на одной системе (RTX 3090 FE, BIOS ASUS 3057), и для работы требуются отдельный EFI-раздел, отключённый Secure Boot и снятый BitLocker. Для 3060 готового проверенного варианта там нет.

Что остаётся неизвестным

  • GA106 и квантованный инференс. Ни у кого нет замера, который разделил бы «int8-ядра» и «деквантование в fp16» для конкретного ядра ggml на sm_86. Ближайшее, что есть, — чужой профиль int8-стека (около 29 % времени в BF16/FP16-ядрах, около 5 % сквозного выигрыша), и он не относится к ggml.

  • Потолок питания. У 3060 значение Max Power Limit зависит от платы и, по внешним отчётам, бывает от 170 до 212 Вт. У нашей платы оно заблокировано на 170, и поднять его нельзя. Возможно, у кого-то есть запас, но проверить это можно только собственной командой nvidia-smi -q -d POWER.

  • Что мы не сделали. Мы не прог

     

    Источник

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов