iiOTESP32 Brasil
Modo de contingência: páginas públicas e downloads seguem disponíveis; login, comunidade e dados do servidor podem ficar indisponíveis.
Voltar para catálogo de tutoriais
Ilustração: uma placa ESP32-S3 sobre a mesa de madeira, com cabo USB, LED verde aceso e uma pequena tela com linhas de texto brilhando, e uma rede neural luminosa se elevando sobre o chip.
Imagem ilustrativa gerada por IA
AvançadoIIoTLeitura: 14 min

Um modelo de linguagem rodando no ESP32-S3

Um transformer fazendo inferência dentro do microcontrolador, sem nuvem: por que a parede é a memória, de onde vem a velocidade e como reproduzir.

Tecnologias:#ESP32-S3#ESP-IDF#llama2.c#ESP-DSP#PSRAM

Requisitos de hardware:

  • ESP32-S3 com PSRAM: 2 MB para o modelo de 260 mil parâmetros, 8 MB para o de 28,9 milhões
  • Flash de 16 MB para o modelo maior
  • Cabo USB com transmissão de dados
  • ESP-IDF instalado (os projetos não usam Arduino nem PlatformIO)

O que este artigo é, e o que ele não é

Aqui um transformer de verdade roda inferência dentro de um ESP32-S3: sem nuvem, sem Wi-Fi, sem servidor do outro lado. Antes de qualquer comando, o aviso que evita frustração: o modelo que cabe no chip não é um assistente.

Os pesos usados nesses projetos vêm do conjunto TinyStories, histórias curtas em inglês com vocabulário reduzido, criadas para pesquisar quão pequeno um modelo pode ser e ainda produzir texto coerente. Ele não responde sobre a sua planta, não lê manual e não substitui o modelo que você usaria na camada de supervisório.

O que ele prova é outra coisa, e essa vale: o silício que já está no seu painel aguenta a matemática de um transformer.

Dois pontos de referência publicados. Em 2024, Dave Bennett rodou um Llama de 260 mil parâmetros num ESP32-S3FH4R2 e mediu 19,13 tokens por segundo. Em 2026, o projeto esp32-ai chegou a 28,9 milhões de parâmetros no mesmo chip, a 9,88 tokens por segundo, ou 94,9 milissegundos por token.

Esses números são dos autores dos dois projetos e não foram medidos aqui. O procedimento descrito neste artigo é o dos repositórios deles, e o que você medir na sua placa é o número que vale para você.

A parede é a memória, não o processador

A armadilha está na compra da placa e pega quase todo mundo na primeira tentativa.

PSRAM não é opcional. Mesmo o modelo de 260 mil parâmetros precisa de cerca de 1 MB de RAM, e o ESP32-S3 tem 512 KB de SRAM interna. Sem PSRAM não roda, e boa parte dos anúncios genéricos de ESP32-S3 DevKit não informa quanto traz, ou não traz nenhuma.

Bennett usou o ESP32-S3FH4R2, que tem 2 MB de PSRAM no próprio encapsulamento. O esp32-ai pede bem mais: 8 MB de PSRAM e 16 MB de flash, porque o modelo ocupa 14,9 MB em quantização de 4 bits.

O ESP32 clássico e o ESP32-C3 ficam de fora deste caminho. O C3 tem um único núcleo, e as rotinas vetorizadas que dão a velocidade aqui são específicas do S3, como se vê na seção do código.

A hierarquia que faz 28,9 milhões de parâmetros caberem

O salto de 260 mil para 28,9 milhões não veio de um chip melhor. Veio de decidir onde cada parte do modelo mora.

São três níveis. A SRAM, rápida e minúscula, guarda as ativações e os pesos de normalização, tocados muitas vezes a cada token. A PSRAM, intermediária, guarda o núcleo do modelo e a cabeça de saída, lidos uma vez por posição. A flash, enorme e lenta, guarda a tabela de 25 milhões de parâmetros.

O truque que torna isso viável se chama per-layer embeddings, emprestado da família Gemma. A tabela de embeddings costuma ser o maior bloco único de um modelo, e é justamente ela que vai para a flash. A cada token, apenas cerca de 450 bytes são buscados de lá. A flash é lenta, mas 450 bytes por vez não pesam.

Vale registrar a inversão: a peça maior foi para a memória mais lenta e funcionou, porque o que importa não é o tamanho do bloco, e sim quantas vezes ele é lido.

Onde o tempo é gasto

Num transformer, a maior parte do tempo é multiplicação de matriz. O llama2.c original faz isso com um laço escalar, que é portátil e lento.

No ESP32-S3 existe um caminho mais rápido: a biblioteca ESP-DSP traz rotinas de produto escalar escritas para as instruções SIMD do núcleo LX7. A função usada é dsps_dotprod_f32_aes3, e o sufixo aes3 indica exatamente a variante do S3. É ela que dá a velocidade, e é por isso que o C3 não chega ao mesmo resultado.

Este é o núcleo da função, no arquivo main/llm.c do repositório de Dave Bennett:

code.cpp
cpp
1void matmul(v4sf *xout, v4sf *x, v4sf *w, int n, int d)
2{
3 *matmul_params = (MatMulTaskParams){xout, x, w, d / 2, d, n, d, TASK_1_BIT};
4 xSemaphoreGive(semaDataReady);
5 for (int i = 0; i < d / 2; i++)
6 {
7 v4sf val = 0.0f;
8 v4sf *row = &w[i * n];
9 dsps_dotprod_f32_aes3(row, x, &val, n);
10 xout[i] = val;
11 }

Os dois núcleos, na prática

Repare no que a função faz antes de começar a contar. Ela preenche matmul_params e libera um semáforo, e só então entra no laço.

Esse semáforo acorda uma tarefa fixada no outro núcleo, que processa a metade de baixo das linhas enquanto o núcleo chamador processa a metade de cima. O corte é literal: d é o número de linhas, e cada lado fica com d / 2.

É a diferença entre dizer que o projeto usa os dois núcleos e mostrar onde isso acontece. Sem o semáforo, o segundo núcleo ficaria parado durante a parte mais cara da inferência.

Meça, não confie na impressão

A sensação de rapidez engana, ainda mais quando o texto sai palavra por palavra na tela. O próprio port já traz a conta pronta, ao fim da geração:

code.cpp
cpp
1float tks = (pos - 1) / (double)(end - start) * 1000;
2fprintf(stderr, "achieved tok/s: %f", tks);

O tempo é medido em milissegundos, daí a multiplicação por mil. Anote o número da sua placa antes de mexer em qualquer configuração: sem a medida de antes, não há como saber se a mudança seguinte ajudou ou atrapalhou.

Passo a passo

O caminho abaixo é o dos repositórios originais. Os dois usam ESP-IDF, e não Arduino nem PlatformIO. Vale saber disso antes de começar, porque a estrutura de projeto é outra.

1. Confirme a placa antes de comprar

Procure a especificação de PSRAM no anúncio, não apenas o nome do chip. Para reproduzir o projeto de 260 mil parâmetros, o mínimo é 2 MB de PSRAM. Para o de 28,9 milhões, são 8 MB de PSRAM e 16 MB de flash.

Se o vendedor não informa a PSRAM, considere que não tem. Essa é a causa mais comum de o projeto não subir.

2. Instale o ESP-IDF

Siga o guia oficial da Espressif para o seu sistema e confirme que o ambiente está ativo antes de seguir. Todo o restante depende disso funcionando.

3. Compile

Com o repositório clonado e o ambiente do ESP-IDF carregado, a compilação é a padrão da ferramenta:

code.cpp
cpp
1idf.py build

A primeira compilação demora, porque baixa e monta as dependências, incluindo a ESP-DSP.

4. Grave e acompanhe

Troque a porta pela da sua placa. No Linux costuma ser algo como ttyUSB0 ou ttyACM0, e no Windows é uma porta COM:

code.cpp
cpp
1idf.py -p /dev/ttyUSB0 flash monitor

O que você deve ver: o monitor abre, a placa reinicia e o texto começa a sair token a token, em inglês, no estilo de história infantil. Ao final da geração aparece a linha com o valor de tokens por segundo.

Se o texto sai, mas em ritmo muito abaixo do esperado, o problema costuma estar na configuração e não no código. Confira se a CPU está em 240 MHz, a PSRAM em 80 MHz e se o cache de instruções foi ampliado, que são os três ajustes citados pelo autor como responsáveis pelo ganho.

5. Quando não couber

Falha de alocação no boot, ou erro de memória durante o carregamento do modelo, aponta quase sempre para o mesmo lugar: PSRAM desabilitada no menuconfig ou placa sem PSRAM nenhuma.

Vale conferir também o tamanho da partição. No caso do modelo de 14,9 MB, uma tabela de partições padrão não dá conta, e o firmware precisa de espaço para a tabela de embeddings na flash.

Onde isso encosta na automação

Sejamos claros sobre o estado da arte: hoje isto é demonstração, não produto. Um modelo TinyStories não tem lugar num painel elétrico.

O que ele antecipa, porém, é concreto. As mesmas rotinas da ESP-DSP e da ESP-NN que aceleram este transformer são as que sustentam inferência útil na borda: detecção de anomalia em vibração de motor, classificação de som e contagem por visão simples. Quem entende como o produto escalar vetorizado e a divisão entre núcleos funcionam aqui está a um passo de aplicá-los ali.

E há a lição de arquitetura, que não depende de modelo nenhum: colocar o bloco maior na memória mais lenta pode ser a decisão certa, desde que ele seja lido poucas vezes. Isso vale para embeddings na flash e vale para histórico de processo em cartão, no seu datalogger.

Ver mais tutoriais técnicosIIoT ESP32 Brasil — Documentação Prática