Год назад на проекте гуманоида с Jetson Orin на борту меня регулярно спрашивали одно и то же. “Можно ли гонять нашу VLA локально, без сети?” VLA это модель. Она берёт кадр и текст и выдаёт команды на приводы.
Я листал документацию, тикеты PyTorch и чужие статьи, а когда разобрался, ответил пересчётом. “Теоретически да, но от кадра до движения выходит 0,84 с. Робот за это время успеет упасть, а пользователь решит, что он завис.”
После того гуманоида ко мне не раз приходили за помощью в проектировании. Чаще это была детекция на 30 FPS на плате за $10 или передовая LLM на устройстве.
Нужна документация, местами китайская, которую приходится гонять через переводчик, и нужны подробные инженерные расчёты. Пиковые тераоперации из даташита почти не двигают задержку от кадра до движения.
Дальше речь про вычислитель. На каждое решение я смотрю, сколько байт едет по шине памяти и что влезает в память. Отдельно смотрю, что остаётся от скорости после прогрева и как это планировать. Теорию ИИ оставляю в стороне. Приводы, механику и безопасность трогаю ровно настолько, насколько они попадают в бюджет задержки. Синтеза траекторий, контура баланса и сертификации тут не будет. Механика появится одной строчкой в таблице.
На Jetson Orin NX 16 GB я поставил GR00T N1.5, вышедшую 11.06.2025, и ничего в сборке не оптимизировал. Дальше я разбирал путь от кадра до первого движения по стадиям и смотрел, сколько времени съедает каждая.
Откуда взялись 0,84 секунды?
Полностью это vision-language-action. Сначала зрительно-языковая часть читает кадр, а дальше за дело берётся модуль действий (DiT). Устроен модуль действий как диффузионный трансформер и собирает план за несколько шагов denoise.
У GR00T N1.6 около трёх миллиардов параметров, а веса 16-битные. Настройки модуля действий я оставил по умолчанию, камер на стенде две, шагов denoise четыре.
Стенд под таблицу 1 был такой. Orin NX 16 GB в пассивном корпусе, две камеры 1280×720 по USB 5 Gbps (USB 3.2 Gen 1), четыре шага --denoising-steps, профиль 25W через nvpmodel и jetson_clocks. Прогонов вышло 24, в таблицу я вынес медиану. За один шаг denoise модуль действий прогоняется целиком и перечитывает веса.
Профиль 40W я не поднимал. Все числа ниже сняты в 25W.
Медиана 836 мс относится к холодному модулю. 24 прогона по 0,84 с занимают около 20 секунд под нагрузкой, а радиатор выходит на режим за минуты. Отбрасывание первых прогонов снимает разогрев кэшей и аллокаторов, но не тепловой выход на режим. Устойчивые числа даёт получасовой прогон с логом tegrastats, до него я не дошёл.
Метрика t_вывода у меня покрывает весь вывод сети. Туда входят энкодер зрения вместе с языковой частью, модуль действий (DiT) и постобработка. Внутри неё модуль действий отдаёт 16 команд на следующие 800 мс движения.
|
Стадия |
Время |
Чем ограничена |
|---|---|---|
|
Захват и предобработка двух кадров по USB |
22 мс |
Протокол USB и копии буферов. В продукте здесь стоял бы CSI, камерный вход на плате |
|
Собственная задержка камеры (экспозиция, вычитывание, UVC, буфер драйвера) |
не мерил |
Для USB UVC на 30 FPS это обычно один-два периода кадра, 33-70 мс. Именно её и снимает переход на CSI |
|
Энкодер зрения на две камеры плюс языковая часть |
490 мс |
Две камеры 1280×720, Orin NX |
|
Модуль действий DiT, 4 шага denoise, план из 16 команд |
144 мс |
Упирается в запуск ядер. Разбор ниже |
|
Постобработка и передача в контроллер |
12 мс |
Границы процессов |
|
Зазоры между стадиями |
80 мс |
Ожидание очереди GPU и синхронизация CPU с GPU |
|
Отработка команды приводом до видимого движения |
88 мс |
Механика и окно интерполяции |
|
Итого до движения |
836 мс |
|
|
Из них на вычислителе |
748 мс |
Без приводов, 22 + 490 + 144 + 12 + 80 |
|
Длина готового плана |
800 мс |
16 будущих команд. Частота команд 20 Гц, шаг 50 мс. 16 шагов по 50 мс |
Таблица 1. Наивная сборка на Orin NX 16 GB. Параметр action_dt задаёт шаг команды 50 мс, то есть частоту 20 Гц.
Все числа таблицы 1 сняты в профиле 25W, и смена профиля меняет их непропорционально. Энкодер и модуль действий проседают по-разному. Разбор по профилям я даю в разделе про классы платформ.
Со стороны софта профиль 40W требует образа Super и JetPack 6.2+. Со стороны платы VDD_IN не ниже 8 В. Выполнить нужно оба условия сразу.
Абсолютный максимум по входному току модуля 5 А, а 40 Вт при 5 В дали бы 8 А, поэтому NVIDIA внесла ограничение отдельным примечанием в datasheet. На пятивольтовой несущей плате профиль 40W не поднять.
Питание у меня было 12 В, а вот образа Super на модуле не стояло, так что весь стенд считал в 25W.
Захват 22 мс, постобработка 12 мс, зазоры 80 мс и отработка приводом 88 мс сняты на стенде вместе со всей таблицей 1. Чужих замеров под эти четыре строки я не нашёл, а p95 у меня есть только по захвату и зазорам.
Два кадра YUV420 на 30 FPS складываются в 2 * 1,38 MB * 30 = 83 MB/s, то есть 664 Мбит/с. Против линка USB 5 Gbps запас семикратный. 22 мс дают протокол и копии буферов. Полоса USB при этом не занята.
Мои 836 мс идут от готового кадра в буфере хоста, а не от события перед камерой. Собственную задержку камеры я не мерил, для этого нужен светодиод в кадре и фотодиод на выходе, поэтому в сумму эта задержка не вошла. На реальном роботе путь от события до движения длиннее на эту задержку.
Ограничение стенда
Замеры я снимал перед тем, как вышел из проекта. Аппаратная ревизия гуманоида в проекте могла отличаться.
Сначала я положил рядом свой прогон без TensorRT и числа NVIDIA, снятые на том же модуле.
|
|
NVIDIA Orin, TensorRT |
NX, без TensorRT |
|---|---|---|
|
Платформа |
Jetson AGX Orin 64 GB |
Orin NX 16 GB |
|
Память и полоса |
64 GB, 204,8 GB/s |
16 GB, 102,4 GB/s |
|
Вход |
1 вид |
2 камеры 1280×720 по USB |
|
Энкодер зрения и языковая часть |
95 мс |
490 мс |
|
DiT |
TensorRT, 4 шага |
без TensorRT, 4 шага |
Таблица 2. Сравнение стендов. Цифры NVIDIA из TensorRT.
Эталонным стендом NVIDIA прямо называет Jetson AGX Orin 64 GB. Цифра 64 GB тут про память модуля. С ключом --workspace 4096 из их примера сборки она никак не связана, тот задаёт рабочую область сборщика TensorRT в мегабайтах. Полосу памяти и арифметику обоих модулей я взял из их datasheet.
У NVIDIA бэкбон стоит 93 мс с torch.compile и 95 мс с TensorRT. Бэкбон под TensorRT у меня не собирается. Мои 490 мс сняты без него. Против их 95 это ожидаемо. У меня два вида вместо одного, бэкбон без TensorRT, профиль 25W.
У них один вид, у меня две камеры. На одной камере я снял 272 мс, на двух 490 мс. Второй вид стоит дешевле полного второго энкодера.
Полоса памяти из datasheet 204,8 GB/s у AGX Orin и 102,4 GB/s у Orin NX. 5,3 TFLOPS FP32 у AGX и 1,88 TFLOPS у NX там же даны как пик MAXN. Я мерил в профиле 25W.
Свои 144 мс на модуле действий я сначала хотел объяснить шиной. 1B параметров в BF16 весят 2 GB, четыре шага, 102 GB/s, отсюда нижняя граница 4 * 2 / 102 = 78 мс.
Проверка эту версию не подтвердила. На одном и том же AGX Orin модуль действий идёт 202 мс в чистом PyTorch, 101 мс с torch.compile и 72 мс с TensorRT. Веса и разрядность те же. Компилятор шину памяти не расширяет. Упор в запуск ядер и мелкие матричные умножения.
В DiT 32 слоя при hidden 512. На каждом из четырёх шагов это сотни крошечных ядер.
Мои 144 мс на более слабой плате для чистого PyTorch неожиданно малы. Что там сидело, покажет совместный прогон. Проверять буду, не включился ли torch.compile по умолчанию, стоит ли горизонт 16 и попадают ли в таймер энкодер и модуль действий вместе с torch.cuda.synchronize(). Скорее всего, у меня был включён torch.compile.
У этой догадки есть цена. Если torch.compile уже сидел в тех 144 мс, шаг компиляции в таблице 9 этот выигрыш второй раз не даст. Удвоение уже в замере. Поэтому совместный прогон стоит у меня раньше квантизации.
Стек DiT по таблице гиперпараметров на странице архитектуры N1.6 набирает 0,15-0,2 млрд параметров, то есть 0,3-0,4 GB в BF16. Размер сходится с той же версией. Формулировка про 2B VLM плюс 1B action head стоит на той же странице, но с её же конфигурацией не сходится.
Мои 2 GB показывают размер файла, в который входит не только DiT. По настоящему стеку нижняя граница по шине выходит 4 * 0,35 / 102 = 14 мс против измеренных 144 мс, то есть версия про чтение весов промахивается на порядок, а не на треть. Точный размер я сниму уже с экспортированного dit_model.onnx.
torch.compile от этого выходит на первое место в списке оптимизаций, он даёт около двух раз даром и качества действий не трогает.
Модуль действий на каждом шаге заново ходит кросс-вниманием по зрительно-языковым токенам, а половина из 32 блоков DiT это как раз кросс-внимание. Значит, вторая камера удорожает и энкодер, и каждый шаг denoise. И замеры NVIDIA сняты на одном виде, у меня видов два.
Политика считает сразу пачку команд вперёд, а горизонт в архитектуре N1.6 равен 16 будущим командам. Шаг команды я при этом не выбираю произвольно. Действия внутри чанка идут по кадрам того датасета, на котором модель дообучали, так что шаг равен единице, делённой на частоту этого датасета. У меня частота равна 20 Гц, шаг 50 мс, отсюда 16 * 50 = 800 мс. Если дообучать на 30 Гц, те же 16 шагов дадут 533 мс, и бюджет придётся пересчитывать целиком.
Расчёт заканчивается перед передачей 16 готовых команд приводам, и из бюджета готового плана 800 мс на сам расчёт уходит 748 мс. Приводы не грузят вычислитель, но входят в задержку реакции робота.
Модель предсказывает 16 шагов вперёд, и это её горизонт предсказания. Сколько из них робот отыграет до следующего плана, это горизонт исполнения, и он обычно короче. У π0 предсказывают 50 действий, а исполняют 16 из них на роботах 20 Гц. В примере запуска GR00T стоит --action-horizon 8 при тех же 16 предсказанных. Причина простая. 800 мс без обратной связи на манипуляции это много, за это время объект уезжает, а контакт срывается.
Поэтому бюджет я держу против горизонта исполнения. При восьми исполняемых шагах он равен 400 мс, а мои 748 мс расчёта не влезают почти вдвое.
Полный путь 836 мс длиннее готового плана 800 мс на 36 мс.
На вычислителе до длины плана остаётся 52 мс, 800 минус 748.
На стенде p95 по захвату кадров и по зазорам между стадиями выше медианы на 12%. p95 это почти самый медленный из 24 прогонов таблицы 1. Вместе эти стадии дают 102 мс. Хвост съедает 12 мс из 52 мс, то есть четверть. Если такой же хвост есть на энкодере и модуле действий, из 52 мс не остаётся вовсе, но p95 по ним я не снимал.
На том же Orin NX параллельно с VLA крутится цикл компьютерного зрения с периодом 33 мс, или 30 Гц. Значит, GPU на стенде занят не одной VLA. Я запустил мелкий детектор и VLA вместе на одном GPU и посмотрел, что стало со зрением. FPS просел на 28%, а период кадра вырос с 33 мс до 46 мс. Обе задачи шли через одну очередь, и вторая ждала, хотя в задержку от кадра до движения эта строка не попадает.
Замер этот снят в щадящем режиме, VLA запускалась не непрерывно, а раз в несколько секунд. При загрузке 748 мс из каждых 800 зрение теряло бы не 28% кадров, а почти все.
Политика считает следующий план с нахлёстом, пока робот ещё выполняет текущий. Без нахлёста робот ждёт конца текущего плана, и между планами дыра длиной во весь расчёт на вычислителе, то есть 748 мс.
Соседние чанки диффузионной политики могут выдать разные формы движения, и за нахлёст приходится платить стыком. На переходе команда прыгает, движение получается рваным и вне обучающего распределения (Real-Time Chunking). Усреднение перекрытий из ACT на больших задержках само раскачивает привод. В Real-Time Chunking предлагают замораживать действия, которые точно успеют исполниться за время вывода, и дорисовывать остаток под этот префикс. Переобучения это не требует.
Помимо тактов GPU эти задачи делят и память. Сами веса VLA занимают 6 GB, а при запуске, по architecture, занято около 12 GB, три четверти модуля на 16 GB.
Linux на Orin NX видит около 14,8 GiB из паспортных 16 GB. Остальное забирает carveout, скрытый резерв прошивки. Против доступного бюджета выходят те же три четверти, так что carveout картину почти не меняет. Разложить эти паспортные 12 GB на CUDA и саму модель я не берусь, сам не мерил. Детектор и два видеобуфера должны уложиться в остаток, а операционная система держит в резерве ещё 10-20%. Схема тракта на рис. 1.
Рис. 1. Тракт данных на Orin NX. Диск, память, шина, три потребителя.
Один только контур баланса не делит с политикой ни очередь, ни память. Он считает стабилизацию тела непрерывно, на своём вычислителе, мимо GPU политики и мимо её задержки.
Пять циклов
В таблице пять строк, и у каждой свой период. Пока быстрые контуры управляют приводами, медленная VLA строит новый план. Общая цифра тут не поможет, поэтому я проверяю срок у каждого цикла по его собственному периоду. Нижний ярус двухслойный, PID в драйверах суставов и управление всем телом идут на одной частоте, но в разных местах и с разными задачами.
MCU крутит свой PID без всякой нейросети, так что про нижний контур говорить почти нечего.
|
Цикл |
Период |
Где считается |
Что будет, если опоздали |
|---|---|---|---|
|
Контур суставов и драйверов |
1-2 мс (500-1000 Гц) |
MCU с PID на отдельном контуре. Нейросеть здесь не нужна |
Ошибка слежения PID растёт |
|
Тело / WBC (управление всем телом) |
1-2 мс (500-1000 Гц) |
Отдельный вычислитель, жёсткое реальное время |
Ломаются контакт и распределение усилий, робот срывается в падение |
|
Траектория и походка (MPC) |
10-50 мс (20-100 Гц) |
Тот же отдельный вычислитель |
Шаг ставится не туда, теряется устойчивость на манёвре |
|
Зрение и препятствия |
33 мс (30 FPS) |
GPU или нейропроцессор (NPU) модуля |
Реакция на препятствие запаздывает на кадр-два |
|
Политика VLA |
Цель 200-1000 мс (1-5 Гц) |
Тот же модуль, фоном |
План устаревает |
Таблица 3. Периоды циклов гуманоида.
Рис. 2. Кто чем занят внутри робота.
Эти периоды и задают рамку, а дальше весь вопрос в том, влезает ли в неё вычислитель.
Политика VLA стоит в таблице ниже всех, идёт фоном на том же модуле и целится в 200-1000 мс. Я беру бюджет не из этого диапазона. Мои 800 мс задают длину готового плана, и я отмерил этот срок на весь путь до движения, вместе с отработкой привода.
Совместный прогон детектора и политики уронил частоту кадров на 28%, и перебор у зрения я считаю из того же замера выше. Период вырос до 46 мс. Против собственного периода 33 мс это 13 мс, и план к моменту исполнения успевает устареть.
Под этими 28% стоит та же карточка, что и под остальными моими числами. Тот же Orin NX 16 GB в пассивном корпусе, профиль 25W с фиксированными частотами. Детектор и политика запущены одновременно и делят одну очередь к GPU, n = 24. Метрикой я взял медиану частоты кадров детектора за минуту установившейся работы.
Период тут не равен допустимой задержке реакции, потому что, пока считается кадр, камера уже отдаёт следующий, и конвейер вытягивает период даже при задержке в разы больше. Что именно отсекает варианты по времени, я разбираю в разделе про облако.
Про STO, категории останова и почему политика и зрение не входят в защиту сюда не кладу. Если будет интересно, опубликую этот расчёт отдельно.
Можно ли считать в облаке вместо борта?
Разница между периодом и задержкой реакции, о которой я говорил выше, здесь и решает всё. Сам период облако бы вытянуло конвейером. Пока ждём ответ по кадру N, уже отправляем N+1. Отсекает облако задержка реакции на препятствие. Заказчику я обещал реакцию не больше 100 мс от появления объекта в кадре, и в эти 100 мс дорога до дата-центра и обратно не влезает.
Паспорт YOLO26n на T4 даёт только время вывода в дата-центре, а замеров на RTX 4090 и H200 у меня нет. Дорога до дата-центра складывается из кодирования кадра, сериализации в аплинк, круговой задержки, декодирования с выводом и обратного пути. Медиана круговой задержки в мобильных сетях по бенчмарку Ookla 2026 лежит между 30 и 50 мс. Для бюджета 100 мс этого слагаемого ещё хватает. Дальняя нога до гиперскейлера в том же отчёте доходит до 150-164 мс в отдельных регионах. На ней бюджет 100 мс кончается, и остальные слагаемые можно не считать.
В контуре управления решает хвост TCP, один потерянный сегмент стоит повторной передачи. Минимальный таймаут повтора в Linux равен 200 мс, это шесть периодов цикла зрения. По TCP такой контур не везут. Везут по UDP или QUIC без повторов, а опоздавший кадр просто выбрасывают. И даже тогда остаётся p99 самого радиоканала. Тот же отчёт Ookla показывает, что под нагрузкой задержка деградирует резко и неравномерно.
Кадр весит 1,38 MB в формате 1280×720 YUV420, и с полосой выходит та же история. Два кадра стенда я уже считал выше, 664 Мбит/с. В мобильный аплинк, где лучшие сети дают около 58 Мбит/с, это не лезет на порядок, так что сжатие обязательно. С межкадровым кодеком появляется своя пила. Средний P-кадр на 4 Мбит/с весит 133 кбит. На линке 20 Мбит/с сериализация занимает 6,7 мс. I-кадр тяжелее примерно на порядок, и раз в группу кадров один кадр едет под 70 мс. Цифры энкодера я взял с салфетки, а вес I-кадра надо мерить под конкретный профиль.
Рис. 3. Кадр на устройстве против поездки в дальний дата-центр и обратно.
На рис. 3 нарисованы три контура, средний из них сервер в том же помещении. Сырой кадр 1,38 MB по гигабитному Ethernet занимает 11 мс сериализации, круговая задержка внутри коммутируемого сегмента меньше миллисекунды, и в 33 мс такая связка укладывается даже без сжатия. Для стационарной установки схема рабочая, на гуманоиде гигабитного сегмента нет.
У политики, ради которой всё и считалось, бюджет 800 мс, и это в двадцать раз мягче цикла зрения. Своего замера энкодера на серверной карте у меня нет, чужого для этой связки я не нашёл.
Обычно в таких случаях режут модель и шлют признаки с энкодера. У GR00T такой разрез не спасает. Энкодер и есть главный расход, 490 мс из 748, а в облако уедет только модуль действий на 144 мс. Отдавать 19% времени в обмен на полную зависимость от канала я не готов, а если резать до энкодера, придётся опять гнать кадр со всей полосой из расчёта выше.
На гуманоиде кадры в облако не едут. Окупаемость бортового NPU на классе устройств, где картинку передавать можно, я считаю по отдельной методике. Если будет интересно, опубликую этот расчёт отдельно.
Дорожную разметку и постоянный поток с микрофона наружу не отдают, после времени в счёт вступает приватность. Детектор ключевого слова поэтому всегда стоит на устройстве и слушает локально, а в облако уходит только реплика после срабатывания, и то с согласия пользователя. Цикл управления остаётся на борту. Облаку достаются ассистент и генерация, где важнее качество ответа и допустима передача данных.
Допустимое окно обрыва связи я беру из длины готового плана. План живёт 800 мс, и столько же длится весь запас движения без новых команд. Пауза дольше 800 мс оставляет робота на устаревшем плане, а дальше идёт заморозка с демпфированием. Хендовер между сотами такое окно перекрывает. Доступность 99,9% тут мало что значит, потому что важна длина непрерывного провала, а не годовая сумма.
Сравнение классов платформ
Потолки tok/s по RTX 4090, H200 и EPYC, паспорт Snapdragon, Orin NX и STM32 в эту статью добавлять не буду. Здесь останется сводная таблица и дерево выбора. Если будет интересно, опубликую это сравнение отдельно.
Сводная таблица платформ
|
Класс |
Сильная сторона |
Слабая сторона |
Типовая модель |
|---|---|---|---|
|
MCU + NPU (STM32N657) |
Низкое потребление |
4,2 MB RAM |
Детектор в INT8 на 1,5-2 MB, и в SRAM к нему помещается один кадр |
|
Мобильный SoC (Snapdragon 8 Elite Gen 5) |
Уже есть в устройстве |
Просадка скорости после прогрева |
LLM 0,8-4B в INT4 |
|
Промышленный модуль (Orin NX 16 GB) |
16 GB общей памяти и CUDA |
Общая очередь GPU |
Зрение и VLA 3B |
|
CPU (EPYC 9965) |
До 6 TB RAM на сокет |
Низкая пропускная способность на ядро |
Большие MoE (смесь экспертов), например, 397B-A17B |
|
Потребительский GPU (RTX 4090) |
Дешевле при низкой загрузке |
24 GB памяти |
Плотные модели до 27B в INT4 |
|
GPU для дата-центра (H200) |
141 GB HBM3e |
Высокая цена простоя у купленной карты |
MoE класса 109B/17B |
Таблица 4. Классы платформ, которые чаще встречаются.
Цена простоя в строке H200 относится к купленному железу, а при почасовой аренде простоя нет вовсе. Счёт идёт за часы работы, и вопрос переносится на загрузку всего парка.
Параметров в YOLO26n 2,4 млн, и в INT8 веса тянут порядка 2,4 MB. Готовый собранный файл модели STM32Cube.AI тяжелее из-за выравнивания, среды исполнения и буферов, и я кладу его в 2,7 MB. Вместе с двумя кадрами 1280×720 выходит 2,7 + 2,76 = 5,46 MB, в 4,2 MB это не помещается, так что MCU я исключаю. Остальные классы я отсекаю по дереву решений на рис. 4.
Рис. 4. Дерево выбора среди RTX 4090, H200, EPYC 9965, Snapdragon 8 Elite Gen 5, Orin NX 16 GB, STM32N657.
Для гуманоида дерево выводит меня на Orin NX, потому что модуль работает локально и даёт 16 GB общей памяти вместе с CUDA. GR00T N1.6 в бюджет памяти MCU и телефона не влезает. Телефону вдобавок не хватает и бюджета времени. Модель уже выбрана. Остаётся уложить GR00T N1.6 и планировщик в бюджет 800 мс.
Все числа статьи зависят от профиля питания nvpmodel, и разница между профилями не сводится к ваттам. Частоты, число TPC и частоту памяти я беру из таблицы режимов nvpmodel для Orin NX 16 GB. TPC это число графических кластеров GPU. EMC это частота памяти. Последний столбец считаю сам как частоту GPU, умноженную на число TPC и нормированную на мой профиль 25W.
|
Профиль |
GPU МГц |
TPC |
EMC МГц |
Полоса |
Счёт GPU относительно 25W |
|---|---|---|---|---|---|
|
40W, нужен образ Super |
1173 |
4 |
3200 |
102 GB/s |
287% |
|
25W, мой стенд |
408 |
4 |
3200 |
102 GB/s |
100% |
|
15W |
612 |
2 |
3200 |
102 GB/s |
75% |
|
10W |
612 |
2 |
2133 |
68 GB/s |
75% |
Таблица 5. Профили nvpmodel у Orin NX 16 GB и счёт GPU относительно профиля 25W, в котором снят стенд.
Вниз от моих 25W шкала идёт немонотонно. Частота GPU там 408 МГц, это ниже, чем 612 МГц в 15W. Выигрывает 25W числом TPC, их четыре против двух. Память трогает только 10W.
Профиль 40W по этому индексу почти втрое больше того, в котором я мерил, и это самый крупный рычаг во всей статье, крупнее любой правки в коде. Нужен образ Super с JetPack 6.2+. Питания 12 В у меня и так хватает. Индекс таблицы 5 считает только частоту GPU и число TPC. Шина у 25W и 40W одна, EMC 3200, 102 GB/s. Если упор в GPU, 490 мс энкодера делю на 2,87. По индексу это 170 мс, в сумму таблицы 9 не входит. Если упор в шину, 490 почти не сдвинутся. Модуль действий держит запуск ядер. Его 144 мс от профиля я не пересчитываю. Прогона в 40W у меня нет.
За 40W придётся заплатить теплом. Тепловой расчёт, формулы потолков, KV-кэш и закон Амдала сюда тоже добавлять не буду, он объемный. Если будет интересно, опубликую этот расчёт отдельно.
Из двух камер я оставляю одну в прежнем разрешении, сокращаю число шагов denoise до двух и ставлю цель 20 мс для зазоров. Итог этих правок я собираю в таблице 9.
Выбор и оптимизация модели
Что смотрю в модели первым
Дальше я беру самую сильную модель, которая влезает в память и в шину, и гоняю её по трём пунктам. Память, шина, среда исполнения.
Kimi K3 я беру как край отсечения по памяти. На гуманоида её не ставят. У Kimi K3 2,8 трлн параметров, опубликованный снимок весов в MXFP4 занимает 1,56 TB, а по обзорам разброс идёт до 1,63 TB. Память такая модель забивает сразу, и счёт по четыре бита на параметр даёт 2,8 трлн * 0,5 байта = 1,4 TB, то есть на 160 GB меньше. Расхождение это не косметическое, потому что те же 160 GB меняют оценку размера узлов. Сверху к этому идут масштабы блоков и неквантованный служебный стек. В моём счёте их нет, а без них оценка уезжает вниз.
Потребительской карте на 24 GB памяти нужно в 65 раз больше, 1560 GB против 24. Даже в два бита останется 2,8 трлн * 0,25 байта = 700 GB, так что про микроконтроллер с единицами мегабайт тут и говорить нечего.
Активных параметров движок читает на каждый токен всего 104 млрд из 2,8 трлн, то есть 3,7%. Ради этой доли MoE и берут, арифметики и чтения выходит меньше. А все эксперты при этом лежат в памяти, поэтому на устройстве упор идёт именно во вместимость. И даже активная часть в четырёх битах даёт 104 млрд * 0,5 байта = 52 GB чтения на токен, вдвое больше всей памяти той же карты. Всегда активный стек лежит не в четырёх битах, так что честное число выше.
Плотная 9B в INT4 занимает 6,2 GB в памяти, если в четыре бита уехала и выходная голова. Голова в FP16 добавляет к этим 6,2 GB ещё 1,5 GB, потому что её 1 млрд параметров переезжает с 0,5 GB на 2 GB, и в памяти выходит 7,7 GB. За два года модель того же класса потяжелела, было 4,5-5,5 GB, стало 6 с лишним. Словарь вырос до 248 320 токенов, входная таблица и выходная голова вместе тянут 2 млрд параметров, по 1 млрд на каждую матрицу.
По шине на токен едет 4,2 GB, и годится это число только для раздельных матриц именно этой модели. На Snapdragon 8 Elite Gen 5 шина около 85 GB/s, и 4,2 GB превращаются в 49 мс на токен и потолок 20 tok/s. Если оставить выходную голову в FP16, трафик вырастет до 5,7 GB, а потолок упадёт до 15 tok/s.
Такой счёт годится для decode, где токены идут по одному, а на стадии prefill движок один раз прогоняет промпт и упирается в арифметику. Движок читает веса каждого слоя один раз и обрабатывает ими весь промпт целиком. Входная таблица и выходная голова Qwen3.5-9B занимают около 2 млрд параметров. Остальные 6,9 млрд параметров тела модели входят в расчёт по всем токенам промпта, и на 1000 токенов это 2 * 6,9 млрд * 1000 = 1,4 * 10^13 операций.
У GPU Orin NX без образа Super плотных 15 TFLOPS FP16. Это тензорный пик MAXN на 918 МГц, не стенд 25W. На профиле 25W GPU держит 408 МГц, 15 * 408 / 918 даёт примерно 6,7 TFLOPS.
Деление 1,4e13 / 1,5e13 даёт 933 мс на один prefill даже при полной загрузке на 15 TFLOPS. На 25W те же 1,4e13 / 6,7e12 дают около 2,1 с. Это FLOP на пик. Prefill я так не гонял. Значит, 9B на промпт такой длины я не рассматриваю, и 4B по тому же счёту тоже мимо.
Для языковой части класса 0,8B картина другая. Тела в ней 0,8 – 0,25 = 0,55 млрд параметров, на 1000 токенов это 2 * 0,55 млрд * 1000 = 1,1 * 10^12 операций, и при 5 эффективных TFLOPS выходит 220 мс. Пять TFLOPS я беру от пика 6,7 как долю загрузки. Пик Hexagon у флагмана остался без замера, так что на телефоне этот счёт надо повторить со своим числом.
Из потолка 20 tok/s и бюджета в три секунды выходит длина короткой реплики, 20 * 3 = 60 токенов.
Один неподдерживаемый оператор в середине графа уходит на CPU, и каждый его вызов добавляет переход с NPU на CPU и обратно. Поэтому каждого кандидата я прогоняю через компилятор целевой среды. В отчёте видно, какие операции остаются на NPU, а какие сваливаются на CPU.
Размер KV-кэша задаёт архитектура внимания, и она для меня весит больше, чем лишний балл в таблице качества. GQA (grouped-query attention) группирует KV-головы, MLA (multi-head latent attention) сжимает внимание в латентное представление. Оба варианта и гибриды с линейным вниманием сокращают KV-кэш в разы, и от этого сокращения зависит, какой контекст я могу пообещать заказчику.
У маленьких моделей раздутый словарь бьёт по квантизации сильнее, чем два года назад. У Qwen3.5-0.8B таблица словаря даёт 248 320 * 1024 = 254 млн параметров, 32% модели (254 млн / 0,8B). Её обычно не квантуют из-за деградации. INT4 даёт -49% к размеру FP16-весов языковой части, а наивный расчёт по четыре бита обещал -75% к тому же размеру.
Так ведут себя AWQ и GPTQ, где таблица и голова остаются в FP16. У llama.cpp иначе. В Q4_K_M входная таблица и выходная голова уходят в Q6_K, средняя битность выходит 4,89 бита на параметр, и размер падает примерно на 69% относительно FP16. Для 0,8B отсюда выходит разница между потолком 133 и 200 tok/s на шине 102 GB/s, потому что 1,50 GB весов в FP16 превращаются либо в 0,77 GB, либо в 0,50 GB. Так что формат надо называть вместе с числом.
У 0,8B и 4B общая матрица входит в трафик целиком, а у 9B и 27B матрицы раздельные, поэтому входная таблица в трафик уже не попадает. Зато по нему едет выходная голова, и у 27B она тянет 1,27 млрд параметров.
|
Класс |
Веса в памяти и трафик на токен |
Где живёт |
Потолок по шине |
|---|---|---|---|
|
Передовая MoE на 2,8 трлн (Kimi K3) |
1,56 TB в MXFP4 |
Стойка, только облако |
1,56 TB против 24 GB |
|
Плотная 27B |
17,8-17,9 GB, по шине 15,3 GB (тело INT4, выходная голова FP16) |
Потребительский GPU на 24 GB |
Потолок 65 tok/s на 1000 GB/s |
|
Плотная 9B |
6,2 GB памяти, трафик 4,2 GB на токен (с выходной головой в FP16 это 7,7 GB памяти и 5,7 GB трафика на токен) |
Флагман с NPU (Snapdragon 8 Elite Gen 5) |
Потолок 15-20 tok/s на 85 GB/s |
|
Плотная 4B |
3,1 GB, входная таблица и выходная голова используют одну матрицу. Читается всё |
Мобильный и периферийный модуль |
Потолок 27 tok/s на 85 GB/s и 33 на 102 |
|
Плотная 0,8B |
0,77 GB в AWQ или GPTQ, 0,50 GB в Q4_K_M, входная таблица и выходная голова используют одну матрицу |
Jetson-класс и флагман |
Потолок 110 tok/s на 85 GB/s и 133 на 102, в Q4_K_M 170 и 200 |
|
Политика VLA 3B (GR00T N1.6) |
Веса 6 GB. Общая занятая память около 12 GB в BF16 |
Промышленный модуль |
Считается не токенами. Считается шагами denoise ( |
|
Меньше 0,5B |
Сотни MB |
Модуль с внешней RAM |
Потолок задаёт не шина, а SRAM в единицы MB, поэтому там остаются только не-LLM сети и один кадр |
Таблица 6. Классы моделей и потолки по шине.
Память, шина и среда исполнения сразу дают вилку по числу камер, шагам denoise и энкодеру, и внутри одной политики я тяну именно за эти рычаги. Числа по видам и шагам у меня со стенда.
Сокращать число видов или шагов denoise можно только после проверки качества на задаче. На манипуляции гуманоида выкинуть второй вид обычно больнее, чем убрать два шага denoise, из-за окклюзий, руки и глубины.
Прирост качества может съесть половину бюджета времени вывода, поэтому качество я проверяю прикладной метрикой, перплексия тут мало что скажет. На одном детекторе касок квантизация после обучения, она же PTQ, оставила точность первого класса почти той же, а средняя точность мелких объектов просела. Для пользователя важна как раз средняя точность.
Структурная обрезка
Неструктурированная обрезка не ускоряет NPU, потому что вычислительное ядро остаётся плотным. Я режу форму тензоров. Срезаю каналы, головы, блоки, разрешение входа.
На STM32N657 детектор 2,7 MB INT8 вместе с двумя кадрами 1280×720 в YUV420 не помещался в 4,2 MB RAM, слои подкачивались по Octo-SPI. Прогон дал 6,1 FPS при бюджете 30. После обрезки до 1,55 MB горячие слои легли в AXI-SRAM, по Octo-SPI осталась только выходная голова, и тот же стенд показал 23 FPS при том же бюджете 30. Холодный старт до первого кадра занял 510 мс.
Обрезанный детектор 1,55 MB и два кадра YUV420 по 1,38 MB дают 4,31 MB против 4,2 MB SRAM, то есть счёт тут на границе и двух полных кадров там нет даже после обрезки. В SRAM у меня лежит один кадр, а второй буфер держится в пониженном разрешении.
Рис. 5. На MCU обрезка решает вместимость SRAM, на Orin NX убирает блоки и вход.
Когда веса лежат во внешней памяти, как на рис. 5, выигрыш от обрезки я считаю по сокращению трафика весов за один прогон. Для модели, которая целиком влезла в SRAM или в кэш ускорителя, счёт идёт иначе, там время определяют MAC.
У LLM всё устроено иначе, обрезка глубины там выкидывает целые слои и бьёт по качеству заметно сильнее, чем обрезка ширины и числа голов. Зато и задержку она срезает сильнее, потому что убирает целые блоки вместе с синхронизациями.
На Orin NX из исходного стенда структурная разреженность 2:4 не используется. Decode упирается в шину, и сжатый формат везёт по ней меньше весов. Упор в шину разреженность поддерживает. Готовых ядер под decode с батчем 1 нет, с четырёхбитными весами разреженность в движках не сочетается, а после обнуления половины весов нужен восстановительный проход обучения. Про паспорт тут надо помнить отдельно. 100 и 157 TOPS у Orin NX это разреженные числа, плотных остаётся 50 и 78, и из них на GPU приходится 30 и 38, остальное держит DLA.
После любого варианта нужен дообучающий проход, иначе потеря качества съедает всю экономию. В проектах я держусь диапазона 10-30% ширины при структурной обрезке с восстановительным дообучением, и это ходовая эвристика, а не замер. Радикальнее берут дистилляцию или сразу готовую модель поменьше.
Обрезка идёт перед квантизацией, а калибровку я делаю уже по финальной архитектуре. Если форму поменяли после калибровки, всю работу надо делать заново.
Квантизация весов
INT8 на GR00T я кладу только на энкодер, а общие режимы ниже касаются детектора и LLM.
От выбора базы цифры разъезжаются вдвое, поэтому выигрыш я всегда считаю относительно названной базы. DiT на гуманоиде работает в BF16/FP16, от него и отсчитываю. Полный INT8 по весам и активациям стоит у детектора на NPU.
Рис. 6. Один вес в четырёх форматах.
|
Режим |
Что сжато |
Где применяют |
Что даёт |
|---|---|---|---|
|
Веса INT8 или INT4, активации FP16 |
Только веса |
LLM на CPU, GPU и мобильном SoC |
Меньше байт по шине на токен |
|
Веса и активации INT8 |
Всё |
Детекторы и мелкие сети на NPU, а также основной режим серверной выдачи |
В 4 раза меньше трафика относительно FP32 |
Таблица 7. INT8 по весам против INT8 по весам и активациям.
В квантизации только по весам активации остаются в FP16, а в целые уходят одни веса. Так работают llama.cpp, AWQ и GPTQ, причём у AWQ и GPTQ на практике берут четыре бита, W4A16. Восьмибитный вариант W8A16 даёт на decode в 2 раза меньше байт по шине на токен, чем FP16. По битности INT8 должен занимать в 2 раза меньше памяти, чем FP16, а INT4 в 4 раза. Выходная голова на деле остаётся в FP16, поэтому на Qwen3.5-0.8B коэффициент INT8 получается 1,48, а коэффициент INT4 равен 1,95. Число операций при этом не меняется. Экономия идёт по байтам, а умножения остаются на месте.
При батче до 4 выигрыш от квантизации только весов большой, а при батче от 16 почти исчезает. Упор уходит с шины на арифметику, а её квантизация весов не трогает.
Режим с целочисленными весами и активациями уводит в восемь бит и то, и другое, а живёт такой конвейер на STM32N657, Hexagon и дискретных ускорителях. DiT гуманоида я в этот режим не отдавал.
Режимы расходятся на prefill, где модель упирается в умножения и меньшее число байт почти не меняет время. Выигрывает тот режим, у которого сами умножения идут в целых на целочисленных ядрах, а для этого в целые должны уехать и активации. Четырёхкратный выигрыш по трафику относительно FP32 остаётся стороной decode. Заодно падает энергопотребление, и таблица Horowitz для 45 нм даёт экономию в 4-20 раз. Для современного решения эта вилка годится как ориентир по порядку величины. Но выигрыш появится, только если режим умеют и движок, и NPU.
Размер веса при групповой квантизации задаёт формула формата.
бит_на_вес = 4 + (бит_масштаба + бит_нуля) / размер_группы
бит_на_вес = 4 + 24/128 = 4,1875, то есть 0,523 байта на вес
где бит_масштаба и бит_нуля задают биты на масштаб и точку нуля у каждой группы, а размер_группы говорит, сколько весов делят одну пару этих параметров (здесь 128). Числа 4 и 24 тут не универсальны. У AWQ часто нет точки нуля, у GGUF свои форматы блоков со своими накладными расходами, поэтому параметры я каждый раз беру из описания того формата, в который конвертирую.
Рис. 7. Четыре бита на вес на бумаге и 4,1875 в файле.
Я подставил эти параметры для Qwen3.5-0.8B с неквантованным словарём и получил 0,509 GB словаря плюс 0,286 GB остального, итого 0,795 GB против 1,50 GB в FP16. Потолок скорости на decode вырос в 1,89 раза.
Платить за INT4 приходится качеством. Свежие числа по Qwen3.5-4B, AWQ INT4 я взял из техрепорта команды конкурса Efficient Qwen, из его Table 3, а не из официальной сводки организаторов. Размер группы 128 назван там для черновой модели, а у целевой указаны только симметричные групповые масштабы. Группу я смотрю в карточке самого чекпоинта, а не переношу из соседней строки техрепорта.
|
Режим весов |
MMLU-Pro |
IFEval |
GPQA-Diamond |
|---|---|---|---|
|
BF16 |
68,0% |
82,5% |
68,0% |
|
INT4, PTQ |
65,6% |
80,5% |
65,4% |
|
INT4, PTQ плюс дистилляция по сетке |
66,1% |
82,9% |
66,3% |
Таблица 8. Цифры из Table 3 техрепорта команды конкурса Efficient Qwen, доли переведены в проценты.
По GPQA техрепорт даёт разброс ±2,1 пункта на пяти прогонах. Разница там на грани. Просадка на MMLU-Pro в 2,4 пункта стоит увереннее. Дообучение поверх фиксированной сетки квантизации вытягивает следование инструкциям выше исходной BF16, а на знаниевых тестах возвращает от одной пятой до трети потерянных баллов. По знаниям до BF16 модель так и не возвращается.
Строки INT8 в таблице 8 нет. Актуальные сравнения проверяют четыре бита и форматы вроде NVFP4. Восьмибитные веса и два года назад стоили десятые доли балла. В исследовании Qwen3 у Qwen3-8B MMLU держится на 74,7% в FP16. Если сжаты только веса, выходит 74,6-74,7%.
В режиме с целочисленными весами и активациями картина меняется, и числа ниже я взял из Table 2 того же исследования Qwen3, строки метода SmoothQuant. Веса и активации в INT8 дают у Qwen3-0.6B 46,3 балла MMLU против 47,1 в FP16, а связка 4-битных весов с 8-битными активациями обваливается до 30,8. Qwen3-8B держится крепче, 74,0 на W8A8 и 63,2 на W4A8. Читать эти числа надо с поправкой. В тех строках веса квантованы на канал, без групп и без AWQ-масштабов, и тот же 0,6B на весах в 4 бита с активациями FP16 даёт в той же таблице 37,3. Значит, больше половины падения дают веса, а не активации. Отсюда и моё чтение. На NPU восемь бит по весам и активациям качество держат, а четыре бита по весам роняют его тем сильнее, чем меньше модель.
На связку 4-битных весов с активациями FP8 этот вывод не переносится. NVIDIA ставит W4A8 AWQ в графу низкого влияния на точность, рядом с W4A16. Для Orin это всё равно мимо. На Ampere поддерживаются только W4A16 и W8A16 по весам плюс INT8 для KV-кэша, аппаратного FP8 там нет, так что W4A8 у меня в очереди на замер для другого железа.
Статический PTQ я гоняю на неразмеченной калибровочной выборке, и там, где от модели требовали максимальной скорости, начинал я всегда с него. Динамический вариант обходится без калибровочных данных вовсе. Масштабы активаций он считает на лету, поэтому обгоняет неквантованный FP32 и отстаёт от статического.
Классификаторы у меня теряли на PTQ меньше всех, а детектор с мелкими объектами и трансформер зрения просаживались заметно сильнее. После пары таких заходов я перестал рассчитывать на чистый PTQ в зрении и дообучение туда закладываю в бюджет сразу.
Квантизация с дообучением (QAT) остаётся для тех случаев, когда PTQ просел. В прямом проходе работает имитация квантизации. С классификатором мне приходилось поднимать разметку, а с LLM хватало обычной текстовой функции потерь.
Масштаб на канал у свёрток почти всегда давал мне больше, чем масштаб на весь тензор, так что при просадке лезу туда первым делом. Пару раз я укладывался по весам и не укладывался по буферам. Пик RAM держат активации, пока они в FP16.
Постоянные члены квантованного матричного умножения конвертер считает заранее, ещё при конвертации модели. Из-за этого я давно перестал видеть в конвертере формальный экспорт. Конвертация входит в оптимизацию. Алгебра целиком приведена у Jacob et al..
В гайде к чужому NPU размер группы стоял как универсальное значение по умолчанию, и я его взял. На этой настройке потом потерял лишние дни. На стенде оказалось, что лучшая точка зависит от того, помещается ли тензор масштабов целиком в банке SRAM ускорителя. При другой группе тензор оттуда вытеснялся. С тех пор размер группы выбираю по расходу SRAM и перебираю заново под каждый чип, каждый слой и каждый параметр. Перебираю до точки, где скорость упирается в потолок, а точность ещё держится в пределах, которые терпит продукт.
Планирование инференса
Раскладка по железу и по времени
Модель влезает и потолок посчитан. Дальше время уходит на переходах CPU/GPU и в общей очереди GPU. Сверху ложится конкуренция за LPDDR5 и тепловое понижение частоты, которое приходит уже под нагрузкой. Дальше я раскладываю задачи по железу и по времени.
Если RMSNorm считается на CPU, а остальные операции на GPU, каждый вызов добавляет переход между ними. Число вызовов я не мерил, а посчитал. В той сборке движка, с которой я начал, на один токен приходится 48 вызовов, это 24 слоя * 2 нормализации у Qwen3.5-0.8B. Счёт этот для decode, а сам decode на этом стенде я не запускал. Руками тут снята только цена одного возврата на CPU и обратно, 40-80 мкс на оба перехода вместе, предварительно и на стенде.
Само число 48 при этом задаёт пол, а не потолок. Две нормализации на слой я считаю по схеме плотного трансформера, а у гибрида внутри внимания стоят ещё нормализации q и k, и свои нормализации есть в слоях с линейным вниманием. Сколько их там ровно, видно только по графу конкретной сборки, и запас я кладу в полтора-два раза. А вот накладные расходы, которые из этих 48 получаются, я держу наоборот как оценку сверху, потому что часть вызовов движок сливает в одно ядро, то есть в одну запускаемую на GPU программу. Каждый вызов даёт около 4 KB трафика. Движок читает вектор на 1024 элемента в FP16 и записывает результат, а на передачу всех этих данных уходит меньше двух мкс при бюджете токена в десяток мс.
На 48 вызовов приходится 96 переходов, и при 40-80 мкс на вызов все 48 дают от 1,9 до 3,8 мс накладных расходов. Эти 1,9-3,8 мс идут на переходы. Объём вектора эту сумму не меняет.
Рис. 8. Ядро считает плитками, а платим мы за шину и за переходы.
Нормализацию я заношу внутрь ядра матричного умножения, и перехода CPU/GPU не остаётся. Гибридным моделям такое слияние нужнее всего. Слоёв там стало больше, а векторы короче, поэтому арифметики на вызов ещё меньше, а самих вызовов ещё больше, и накладные расходы перехода растут.
Цикл зрения и всё, что считается рядом с ним, делят одну очередь GPU и общую LPDDR5, так что пока очередь занята, зрение ждёт. Ровно здесь я и снял тот замер, с которого начиналась статья. Детектор и политика стояли в одной очереди, и зрение потеряло 28% кадров. Сначала я бы попробовал увести сеть зрения на DLA, тогда языковая часть осталась бы на GPU и задачи перестали бы делить одну очередь.
Набор операций у DLA урезанный, динамических форм нет, а неподдерживаемые слои движок откатывает обратно на GPU, то есть ровно в ту очередь, от которой я и ухожу. Слой сети зрения должен лечь на DLA целиком. Кусок, который DLA не берёт, движок вернёт в ту же очередь GPU.
В зазоры в 80 мс у меня входят синхронизация после каждой стадии, копии через непинованную память между процессами и ожидание очереди GPU, когда детектор и политика приходят одновременно. Разложения по этим трём частям у меня нет, и без него цель в 20 мс висит в воздухе.
Движок режет промпт на порции и между порциями пускает чужую работу. Chunked prefill обходится дешевле, чем уводить языковую часть на CPU. В vLLM он включён по умолчанию, decode идёт первым, а размер порции задаёт max_num_batched_tokens. Длинное ядро prefill занимает очередь целиком. Между порциями очередь отпускает зрение.
На стенде я оставил детектор класса YOLO26n на GPU, а языковую модель класса Qwen3.5-4B перенёс на CPU с векторизацией через ARM NEON. Процессы обменивались данными через общую память. Такое разнесение добавило 9 мс к задержке, зато разница между p95 и p5 времени кадра после него уменьшилась. Эти 9 мс покрывают полную цену разнесения, в них входит и счёт языковой части на CPU. Из них 6 мс приходится на сам межпроцессный обмен.
Контур зрения и ассистент я развёл, и оба они работают вне контура баланса. Сам контур баланса в соседний процесс на том же SoC я не отдаю. Технически так делают, под PREEMPT_RT с выделенными ядрами он на прикладном процессоре живёт. Но на этом модуле у него соседи. Политика с очередью к GPU, детектор и общая LPDDR5, а сверху тепловое понижение частоты. Хвосты задержки в такой компании я предсказать не берусь, поэтому контур баланса у меня уезжает на отдельный микроконтроллер.
Тепловой режим тоже работает планировщиком, и с одним порогом он начинает дёргаться. Переключились на CPU, остыли до 77,9 °C. Вернулись на GPU, нагрелись до 78,1 °C. Переключились обратно. И так десятки переключений в минуту, каждое с перезагрузкой весов и провалом задержки. Зазор между порогами и выдержка по времени убирают этот дребезг. Уходим с GPU при T выше 78 °C, возвращаемся при T ниже 71 °C и не раньше чем через 90 секунд. Зазор не пускает нагрузку назад на горячий GPU из-за десятой доли градуса, выдержка добивает остаток. За час под нагрузкой стенд показал единицы переключений.
Пороги 78 °C и 71 °C я снимаю с зоны gpu-thermal, и стоят они сильно ниже железной точки снижения частот в 99 °C. Предсказуемую задержку я держу порогом 78 °C. На 99 °C частоты уже режет сам чип.
CPU и GPU у Orin сидят на одном кристалле и за одним радиатором, так что перенос нагрузки тепло никуда не уносит. Работает такой перенос только через снижение суммарной мощности, и величину этого снижения надо мерить по входному питанию, а не предполагать.
Размер батча входит в расчёт памяти напрямую. Снижаю батч с 4 до 1 на контексте 4096 у той же гибридной 0,8B, и это освобождает около 175 MB. Из 233 MB на кэш и состояние остаётся примерно 58 MB. Остальное добираю лимитом контекста. Почему лимитом, разберу отдельно, если будет интересно.
Отдельно я бы проверил лимит группы процессов (cgroup), потому что ограниченный им процесс упрётся в нехватку памяти задолго до исчерпания физической RAM, а free будет показывать, что всё хорошо.
Итог для исходного стенда
Таблица 9 собирает шаги оптимизации по порядку.
|
Шаг |
Что меняем в этой сборке |
Эффект |
Статус |
|---|---|---|---|
|
1. Расчёт |
Бюджет 800 мс и память 16 GB |
Арифметика по разбору выше |
Расчёт по таблице 1 |
|
2. Профиль питания |
Образ Super и профиль 40W вместо моих 25W |
Все стадии на GPU, по индексу таблицы 5 почти втрое |
Прогона нет. В сумму ниже не заложен |
|
3. Сокращение входа |
Одна камера вместо двух, разрешение прежнее |
Время энкодера |
272 мс, раздельный прогон |
|
4. Компиляция |
|
Время модуля действий |
Прогона нет. Зависит от того, был ли он уже включён в прогоне на 144 мс |
|
5. Выбор модели |
Два шага denoise вместо четырёх |
Время модуля действий |
72 мс, раздельный прогон на стенде |
|
6. Квантизация |
Энкодер зрения в INT8 |
Освобождает память под детектор |
Расчёт, в сумму стадий не входит |
|
7. Планировщик |
Зазоры с 80 мс до 20 мс, разные очереди GPU |
Цель минус 60 мс |
Прогона нет |
|
Итого на вычислителе (расчёт) |
Сумма шагов выше |
398 мс |
Сложил разнородные строки. 22 + 272 + 72 + 12 + 20. Совместного прогона нет |
|
Итого до движения (расчёт) |
Плюс механика из таблицы 1 |
486 мс |
398 плюс 88 мс механики |
Таблица 9. Расчётная цель после наивной сборки.
Языковую часть из энкодера я отдельно не вычитал.
Модель во всех шагах остаётся GR00T N1.6. NVIDIA на странице TensorRT предупреждает, что два шага denoise могут снизить качество действий. Качество двух шагов на моей сборке GR00T я не мерил.
INT8 кладу только на энкодер зрения, языковую часть и модуль действий оставляю в BF16. Считаю я по параметрам, и энкодер зрения набирает примерно 0,5 млрд параметров, в BF16 он занимает около 1 GB, в INT8 около 0,5 GB, то есть освобождается примерно полгигабайта. Если уводить в INT8 весь бэкбон на 2 млрд, экономия будет около 2 GB, но и риск по качеству другой. Штатного пути тут нет. Сборка TensorRT в GR00T охватывает только модуль действий и знает режимы FP32, FP16, BF16 и FP8, так что INT8 энкодера остаётся отдельной работой со своей калибровкой. Паспортные около 12 GB занятой памяти на эти полгигабайта на бумаге не уменьшатся.
Соседи по классу, π0, OpenVLA и RT-2
Соседи по классу показывают масштаб. У π0 flow matching идёт 10 шагами на чанк из 50 действий, а весь путь на RTX 4090 занимает около 73 мс. Префикс языковой части там считают один раз и кэшируют, а по шагам гоняют только эксперт действий на 300 млн параметров. У OpenVLA на 7 млрд параметров авторегрессия по токенам даёт около 6 Гц на RTX 4090. Diffusion Policy берёт порядка 10 шагов.
У OpenVLA каждое из семи измерений команды разбивают на 256 корзин, и каждая корзина занимает один токен словаря. Отсюда семь токенов на действие. У RT-2 выход это восемь целых, шесть степеней свободы, захват и terminate. Поэтому у авторегрессионных политик длина выхода растёт вместе с горизонтом, а у диффузионных нет.
Заключение
Я собрал здесь все свои основные заметки по этому проекту, уж очень хотелось их объединить и при случае обращаться. Да, получилось много цифр и местами их сложно читать. Что-то я вырезал намеренно и если тематика приживется опубликую отдельно, а то статья в первой редакции получалась на час чтения по оценке хабра.
Буду рад если они и вам помогут в практике.
Про выбор платформ, системную разработку и менеджмент я пишу в The tough case. Следующие две недели там разберу STO, шлагбаум, IPC и MTBF. Которые остались отдельными кейсами этой методики.
Автор: xitren
- Запись добавлена: 20.08.2026 в 13:59
- Оставлено в


