Когда робот тупит, а бюджет не резиновый
Запускаете VLA-модель на роботе, а он реагирует как пьяный слон в посудной лавке? Модель вроде понимает задачу («возьми красный куб»), но вместо плавного движения выдает рывки, замирает на полсекунды или вообще делает что-то свое. Это не глюк — это фундаментальное несоответствие между обучением модели и реальной физикой вашего робота.
Стандартные VLA-модели (Cosmos-Reason2-1.8B, PhysicalAgent) обучены на абстрактных действиях. Они знают, что такое «повернуть направо», но не знают, что ваш конкретный мотор-редуктор имеет люфт в 5 градусов, а камера сдвинута на 3 см от центра тяжести. Результат — команда «повернуть на 90 градусов» превращается в поворот на 87 или 93 градуса, и робот едет не туда.
Философский вопрос: если модель дает правильную команду, но робот выполняет ее криво — кто виноват? В 90% случаев — вы, потому что не дообучили модель под ваше железо.
Fine-tuning без миллиона долларов и дата-сайентистов
Теоретики говорят: «Собери датасет из 100 тысяч пар (изображение-действие) и дообучи модель». Практика показывает, что для embedded-робота хватает 500-1000 примеров, если собирать их с умом. Не верите? Посчитайте: за рабочий день робот совершает ~2000 действий. За неделю — 14000. Выбирайте лучшее — и датасет готов.
1 Собираем датасет без боли
Забудьте про ручную разметку. Ваш робот уже умеет ездить? Отлично. Подключаете логирование:
import json
import cv2
from datetime import datetime
class DataCollector:
def __init__(self, robot):
self.robot = robot
self.buffer = []
def capture_episode(self, task_description, duration_sec=60):
start_time = datetime.now()
while (datetime.now() - start_time).seconds < duration_sec:
# 1. Кадр с камеры
frame = self.robot.camera.capture()
# 2. Текущее состояние (положение, скорость, IMU данные)
state = self.robot.get_state() # [x, y, theta, v_linear, v_angular]
# 3. Действие, которое ВЫБИРАЕТ оператор или автономный контроллер
action = self.robot.get_last_command() # [steering, throttle]
# Сохраняем тройку
sample = {
"timestamp": datetime.now().isoformat(),
"task": task_description,
"image_path": f"frames/{datetime.now().timestamp()}.jpg",
"state": state.tolist(),
"action": action.tolist(),
"success": None # Пометим позже
}
cv2.imwrite(sample['image_path'], frame)
self.buffer.append(sample)
return self.buffer
Ключевой момент — записываете не то, что робот СДЕЛАЛ, а то, что он ДОЛЖЕН БЫЛ СДЕЛАТЬ по команде оператора. Разница принципиальная: модель нужно учить правильным командам, а не ошибкам исполнения.
2 Выбираем, что будем дообучать
Полная перетренировка 1.8B модели на микроконтроллере? Забудьте. Используем LoRA (Low-Rank Adaptation) — дообучаем только 0.5-2% параметров. На i.MX 9 с NPU это делается на самом устройстве за несколько часов.
| Стратегия | Параметры | Память на i.MX 9 | Время обучения (1000 сэмплов) |
|---|---|---|---|
| Full Fine-tuning | 1.8B (100%) | 8+ GB — не влезет | Неприменимо |
| LoRA (r=8) | ~9M (0.5%) | 512 MB | 2-3 часа |
| QLoRA (4-bit) | ~4.5M (0.25%) | 256 MB | 1-1.5 часа |
# Дообучение Cosmos-Reason2-1.8B с помощью LoRA на i.MX 9
from transformers import AutoModelForCausalLM, AutoProcessor
from peft import LoraConfig, get_peft_model
import torch
# Загружаем предобученную модель
model = AutoModelForCausalLM.from_pretrained(
"cosmos-reason2-1.8B",
torch_dtype=torch.float16,
device_map="auto"
)
# Конфигурация LoRA - только attention слои
lora_config = LoraConfig(
r=8, # rank
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # Только attention
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # Покажет ~0.5% параметров
3 Готовим данные для модели
VLA-модель ожидает определенный формат ввода. Cosmos-Reason2-1.8B (2026) принимает мультимодальный input: изображение + текст + предыдущие действия.
# Преобразование данных в формат для обучения
import torch
from PIL import Image
class VLADataset(torch.utils.data.Dataset):
def __init__(self, samples, processor, max_length=512):
self.samples = samples
self.processor = processor
self.max_length = max_length
def __len__(self):
return len(self.samples)
def __getitem__(self, idx):
sample = self.samples[idx]
# Загружаем изображение
image = Image.open(sample['image_path']).convert('RGB')
# Формируем текст: задача + состояние
# Состояние нормализуем к диапазону [-1, 1]
state_norm = self.normalize_state(sample['state'])
state_str = ",".join([f"{v:.3f}" for v in state_norm])
text = f"Task: {sample['task']}. State: [{state_str}]. Action: "
# Действие как продолжение текста
action = sample['action']
action_str = ",".join([f"{v:.3f}" for v in action])
full_text = text + f"[{action_str}]"
# Токенизация
encoding = self.processor(
images=image,
text=full_text,
return_tensors="pt",
truncation=True,
max_length=self.max_length,
padding="max_length"
)
# Сдвигаем labels для обучения next token prediction
encoding['labels'] = encoding['input_ids'].clone()
# Маскируем часть до "Action:" (не учим предсказывать условие)
action_pos = full_text.find("Action:")
if action_pos > 0:
# Упрощенная логика - в реальности нужно работать с токенами
encoding['labels'][:, :action_pos] = -100
return {
'input_ids': encoding['input_ids'].squeeze(),
'attention_mask': encoding['attention_mask'].squeeze(),
'pixel_values': encoding['pixel_values'].squeeze(),
'labels': encoding['labels'].squeeze()
}
Асинхронный inference: когда модель думает, а робот не ждет
Вы дообучили модель. Теперь она знает особенности вашего робота. Но если запустить ее синхронно (камера → модель → действие), получите те же 200-300 мс задержки. Робот продолжит дергаться.
Решение — двойная очередь и прогнозирование. Пока модель обрабатывает кадр N, система управления использует прогноз на основе кадра N-1. Если прогноз хороший, робот движется плавно. Если модель «задумалась» дольше обычного — включается экстраполяция.
4 Реализация на C++ для i.MX 9
Используем eIQ ML Environment 2026 с асинхронным API. Ключевые компоненты:
- Capture Thread: захват кадров с камеры (30 FPS)
- Inference Thread: выполнение модели на NPU
- Control Thread: управление моторами с частотой 100 Hz
- Prediction Buffer: кольцевой буфер на 5-10 последних предсказаний
// Упрощенная архитектура на C++ (eIQ ML 2026 API)
#include
#include
#include
#include
class AsyncVLAController {
private:
std::atomic running{true};
std::deque frame_queue;
std::deque action_queue;
std::mutex frame_mutex, action_mutex;
// Модель VLA
eIQ::Model vla_model;
// Последнее валидное действие для экстраполяции
Action last_action;
uint64_t last_action_ts;
public:
void capture_thread() {
Camera cam("/dev/video0", 640, 480);
while (running) {
Frame frame = cam.capture();
std::lock_guard lock(frame_mutex);
frame_queue.push_back(frame);
// Держим очередь в разумных пределах
if (frame_queue.size() > 5) frame_queue.pop_front();
}
}
void inference_thread() {
while (running) {
Frame frame;
{
std::lock_guard lock(frame_mutex);
if (frame_queue.empty()) continue;
frame = frame_queue.back();
frame_queue.clear(); // Берем только последний кадр
}
// Препроцессинг и запуск на NPU
eIQ::Tensor input = preprocess_frame(frame);
eIQ::Tensor output = vla_model.runAsync(input); // Неблокирующий вызов
Action action = parse_output(output);
{
std::lock_guard lock(action_mutex);
action_queue.push_back(action);
last_action = action;
last_action_ts = get_timestamp_us();
if (action_queue.size() > 10) action_queue.pop_front();
}
}
}
void control_thread() {
MotorController motors;
while (running) {
uint64_t now = get_timestamp_us();
Action target_action;
bool has_new_action = false;
{
std::lock_guard lock(action_mutex);
if (!action_queue.empty()) {
target_action = action_queue.back();
action_queue.clear();
has_new_action = true;
}
}
if (!has_new_action) {
// Экстраполяция на основе последнего действия
// Простая линейная экстраполяция на 10 мс вперед
float dt = (now - last_action_ts) / 1e6f;
target_action = extrapolate_action(last_action, dt);
}
motors.set_action(target_action);
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 100 Hz
}
}
};
// Компиляция для i.MX 9
// arm64-poky-linux-g++ -O3 -mcpu=cortex-a55 -mfpu=neon-fp-armv8 \
// -I/usr/include/eiq -leiq_async -lpthread async_vla.cpp -o async_vla
Важный нюанс: eIQ ML 2026 поддерживает truly асинхронное выполнение на NPU Ethos-U85. Модель загружается один раз, а потом inference.runAsync() не блокирует CPU. В 2024 году такого не было — приходилось выдумывать костыли.
Где спрятаны грабли (и как на них не наступить)
Ошибка 1: Переобучение на шум
Дообучаете модель на данных с реального робота, а в данных есть систематические ошибки (например, мотор всегда недоворачивает на 2 градуса). Модель выучит эту ошибку как норму.
Решение: перед обучением откалибруйте робота. Заставьте его проехать квадрат 1×1 метр и измерьте отклонения. Внесите поправки в данные обучения.
Ошибка 2: Буферы переполняются
Ставите очередь на 1000 кадров, модель не успевает, очередь растет, задержка достигает секунд. Робот управляется кадрами 10-секундной давности.
// НЕ ТАК
queue.push(frame);
if (queue.size() > 1000) { /* ой */ }
// ТАК
queue.push(frame);
if (queue.size() > 3) {
// Берем только последние 3 кадра
queue.pop_front();
// Либо пропускаем inference для этого кадра
// Либо уменьшаем разрешение кадра на лету
}
Ошибка 3: Ignore latency mismatch
Время inference на NPU — 50 мс. Время захвата кадра — 33 мс. Время отправки команды на мотор — 5 мс. Суммарная задержка: 88 мс. Частота управления: 11 Hz. Робот дергается.
Решение — временная привязка (timestamping). Каждый кадр, каждое действие маркируйте временем. Система управления должна знать: «Эта команда для времени T+100мс». Если команда пришла позже — экстраполируйте.
Частые вопросы от инженеров
Q: Стоит ли использовать более новую модель, чем Cosmos-Reason2-1.8B?
A: На март 2026 года Cosmos-Reason2-1.8B остается оптимальной для микроконтроллеров. Более новые модели (Cosmos-Reason3, PhysicalAgent 2.0) требуют больше памяти и дают прирост качества в 5-10%, но не влезают в 2 GB RAM i.MX 9 без сильной квантования.
Q: Можно ли обойтись без fine-tuning?
A: Можно, если ваш робот — точная копия того, на котором обучали модель. В реальности отличия в массе, трении, расположении камеры дают накопленную ошибку в 20-30% за минуту работы. Fine-tuning снижает эту ошибку до 2-5%.
Q: Почему именно асинхронный inference, а не просто ускорение модели?
A: Потому что физический мир не ждет. Даже если вы ускорите модель с 200 мс до 50 мс, это все равно задержка. Асинхронный подход с прогнозированием дает эффективную задерчку 0-10 мс за счет умной экстраполяции. Почитайте детали в нашей статье про асинхронный инференс от NXP.
Q: Как проверить, что fine-tuning сработал?
A: Самый простой тест — «слепой маршрут». Научите робота объезжать 5 препятствий в определенном порядке. До fine-tuning он собьет 3-4 из 5. После — 0-1. Если не помогло — проверьте качество данных: возможно, вы записали ошибки, а не правильные действия.
Что будет через год?
К 2027 году появятся VLA-модели, которые дообучаются онлайн, прямо на роботе. Вы покажете роботу 10 примеров действий рукой (через AR-очки), и он адаптируется за минуту. Потребуются NPU с 10+ TOPS и памятью HBM на чипе, но NXP уже анонсировала i.MX 10 с такими характеристиками.
Главный вызов — не железо, а алгоритмы. Как дообучать модель без катастрофической забывчивости? Как гарантировать безопасность, если модель меняется в реальном времени? Эти вопросы еще ждут ответов. А пока — используйте LoRA и асинхронный inference. Это работает сегодня.