
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.
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)
Baixe o material deste tutorial:
- Kit de apoio (ZIP)12 KBsdkconfig.defaults comentado, tabela de partições para flash de 16 MB, checklist e o manual em PDF. Não contém o firmware: o port do llama2.c é dos autores citados, e o README leva aos repositórios originais.
- Manual (PDF)12 KBAs seis páginas do artigo em formato de leitura offline, com as tabelas de referência, solução de problemas e créditos.
Quer receber novos manuais e esquemas industriais? Entre no nosso canal do Telegram / WhatsApp
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:
| 1 | void 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:
| 1 | float tks = (pos - 1) / (double)(end - start) * 1000; |
| 2 | fprintf(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:
| 1 | idf.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:
| 1 | idf.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.
