TRENDSHIFT DAILY #6 · 2026-07-25 · ESP32-AI · $8 칩 위의 LLM

slvDev/esp32-ai 딥다이브
8달러짜리 마이크로컨트롤러 위에서 28.9M 파라미터 LLM을 돌리는 법

esp32-ai는 손톱만 한 ESP32-S3 마이크로컨트롤러(약 $8) 위에서 28.9M(2,890만) 파라미터 언어모델을 서버 없이 온디바이스로 돌리는 실험이다. 인터넷도, 클라우드도 필요 없이 칩 자체에서 초당 약 9.5개 토큰 속도로 문장을 만들어 칩에 연결된 작은 화면에 한 단어씩 써 내려간다.

이 프로젝트의 급소는 이것이다: "모델을 빠른 메모리에 다 올린다"는 상식을 버리고, 모델 대부분(2,500만 파라미터)을 느리지만 거대한 flash에 남겨 둔 채 토큰마다 필요한 몇 줄(약 450바이트)만 꺼내 쓴다. 이 트릭은 구글 Gemma 모델의 Per-Layer Embeddings(PLE)에서 왔고, 그 아이디어를 스마트폰이나 GPU가 아니라 마이크로컨트롤러의 메모리 계층에 처음으로 적용했다. 같은 급 칩에서 돌린 이전 모델(26만 파라미터)보다 약 110배 큰 모델이다.

(저장소 slvDev/esp32-ai · 학습 Python 3.12 · PyTorch · 펌웨어 C · Arduino ESP32 · 데이터셋 TinyStories · 칩 ESP32-S3 N16R8 · 상태 2026-07-21 종단 검증 완료 · 30 커밋 · shallow clone 소스 직접 분석)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — 물리 한계를 우회하는 발상
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석 — 3단 메모리 회계와 PLE
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

"큰 모델은 큰 컴퓨터에서만"이라는 통념을 8달러 칩으로 뒤집는다

스마트폰의 두뇌인 SoC조차 버거워하는 LLM을, 이 프로젝트는 냉장고 온도 센서에나 들어갈 법한 $8짜리 칩 ESP32-S3 위에서 돌린다. 모델은 TinyStories라는, 3~4세 아이도 이해할 수준의 짧은 영어 동화 데이터로 학습됐다. 그래서 이 모델은 질문에 답하거나 지식을 말하거나 코드를 쓰지는 못한다. 대신 "옛날 옛적에…"로 시작하는 짧고 일관성 있는 이야기를 스스로 지어낸다.

중요한 건 모델이 무슨 을 하느냐가 아니라, 어떻게 그 큰 덩치가 그 작은 칩에 들어갔느냐다. ESP32-S3의 빠른 내부 메모리(SRAM)는 고작 512KB. 보통은 모델 전체가 여기에 들어와야 하니 지금까지 이런 칩에 올린 모델은 26만 파라미터가 한계였다. 이 프로젝트는 그 벽을 "모델을 빠른 메모리에 다 넣지 않는다"는 발상으로 넘어, 2,890만 파라미터를 앉혔다.

핵심 비유

"책상 위에 책을 다 쌓는 사람" vs "서가에 두고 필요한 페이지만 꺼내는 사람"

보통의 방식은 작업에 필요한 모든 책을 좁은 책상(SRAM) 위에 다 올려두는 것과 같다. 책상이 작으면 올릴 수 있는 책도 적다 — 그래서 모델이 작아야 했다.

esp32-ai는 거대한 서가(flash)에 대부분의 책을 꽂아 두고, 지금 이 순간 필요한 몇 페이지(약 450바이트)만 책상으로 가져온다. 서가는 느리지만, 한 번에 몇 페이지만 꺼내면 되니 사실상 공짜다. 덕분에 책상 크기는 그대로인데 다룰 수 있는 책(모델)은 100배로 커진다.

용어 풀이
마이크로컨트롤러 (MCU, Microcontroller)
CPU·메모리·입출력이 칩 하나에 다 들어간 초소형 컴퓨터. 세탁기·에어컨·IoT 센서 같은 곳에 박혀 특정 일만 한다. PC와 달리 메모리가 KB~MB 단위로 극히 작고, 운영체제도 없거나 아주 가볍다. ESP32-S3는 이 부류에서 흔히 쓰이는 Wi-Fi 내장 칩으로, 가격이 몇 달러 수준이다.

2왜 주목받는가 — 물리 한계를 우회하는 발상

TrendShift Daily 상위권(#6)에 오른 이유 4가지

"작은 칩에 큰 모델"이라는 주제는 늘 인기지만, 이 레포가 트렌딩에 오른 건 단순한 데모가 아니라 진짜 과학이기 때문이다. 네 가지 결이 겹친다.

① 물리적 한계를 정면돌파가 아니라 '우회'로 뚫었다

더 빠른 칩을 사거나 모델을 무조건 줄이는 대신, 메모리의 성질이 저마다 다르다는 점을 이용했다. 언어모델 파라미터의 대부분은 계산에 쓰이는 게 아니라 단순히 찾아보는 표(embedding table)다. 그 표는 토큰마다 딱 몇 줄만 읽으면 되니, 굳이 빠른 메모리에 둘 필요가 없다. "느린 flash에 둬도 되는 것"과 "빠른 SRAM에 꼭 둬야 하는 것"을 가른 것이 이 프로젝트의 전부다.

② 정직한 과학 — 자기 실수까지 커밋에 남겼다

주장을 검증하는 방식이 놀랍도록 성실하다. PLE가 baseline보다 나은지 2개 시드로 재현했고, 4-bit로 압축해도 이득이 살아남는지 따로 측정했으며, C로 짠 추론기가 PyTorch 원본과 3만 2768개 logit 전부에서 최대오차 0.00001로 일치함을 하드웨어에 올리기 전에 확인했다. 심지어 저자는 초기 파라미터 계산 버그로 숫자를 부풀렸던 이력과 그걸 고친 결과를 일부러 저장소에 남겨 뒀다.

③ "$8 칩" 서사 — 엣지 AI의 상상력을 자극한다

클라우드도, GPU도, 인터넷 연결도 없이 몇 달러짜리 부품이 스스로 문장을 만든다는 그림은 완전한 오프라인·프라이버시 보존 AI의 원형을 보여준다. 데이터가 칩 밖으로 한 바이트도 나가지 않는다. 이 서사가 개발자 커뮤니티(특히 임베디드·IoT 진영)를 자극했다.

④ Karpathy llama2.c의 계보 — "plain C로 돌린다"는 감성

저자는 Andrej Karpathy의 llama2.c(순수 C로 작은 LLM을 돌리는 프로젝트)가 "작은 언어모델을 직접 학습해 C로 돌릴 수 있다"는 믿음을 준 계기였다고 밝힌다. 무거운 프레임워크 없이 바닥부터 직접 짠다는 이 문화적 계보가, 같은 감성을 공유하는 개발자들에게 매력적으로 다가온다. (단, 저자는 코드·방법이 llama2.c에서 파생된 건 아니라고 명확히 선을 긋는다 — 비교 맥락일 뿐이다.)

한 걸음 더 — 숫자를 '정직하게' 다룬다

저자는 "28.9M 파라미터"가 무엇을 뜻하고 무엇을 뜻하지 않는지까지 못박는다. 이건 저장된(stored) 파라미터가 2,890만이라는 뜻이지, 능력이 100배라는 뜻이 아니다. 여전히 TinyStories 도메인 모델이라 세상 지식·산수·다단계 추론은 못 한다. "메모리 계층 분할로 상주시킨 파라미터 수"라고 부르지, 절대 "능력 배수"로 광고하지 말라고 스스로 적어 둔다. 이 절제가 오히려 신뢰를 높인다.

구분전형적 온디바이스 LLMesp32-ai (PLE)
모델 크기 한계빠른 메모리(SRAM) 용량에 갇힘느린 flash 용량까지 확장 (약 110배)
어디에 모델을 두나전부 빠른 메모리에 상주계산 코어만 SRAM, 표는 flash에
토큰당 flash 접근거의 안 함(다 RAM에 있음)표에서 6줄(~450B)만 읽음 (~0.12ms)
연결성보통 클라우드 보조완전 오프라인, 칩 밖으로 0바이트
검증 태도데모 위주2시드·4bit·host-golden 1e-5 매칭

3기술 스택 전체 지도

두 세계가 만난다 — PyTorch로 '학습', 순수 C로 '칩에서 추론'

이 저장소는 완전히 다른 두 파이프라인이 한 몸으로 붙어 있다. 하나는 데스크톱에서 모델을 학습·양자화·검증하는 Python(PyTorch) 세계이고, 다른 하나는 그 결과물을 칩 위에서 실제로 돌리는 C(Arduino) 세계다. 둘을 잇는 다리가 export.py가 뽑는 납작한 바이너리(model.bin)와, 호스트와 칩에서 같은 코드가 도는 단일 헤더 llm.h다.

학습 쪽 의존성 (pyproject.toml — Python 3.12, uv 패키지 매니저)

# name = "esp32-llm"  ·  requires-python = ">=3.12"
numpy      >=2.5.1     # 토큰 bin(memmap) 처리
requests   >=2.34.2    # TinyStories 데이터 다운로드
tokenizers >=0.23.1    # BPE 토크나이저 학습
torch      >=2.13.0    # 모델 학습/양자화/golden 생성
tqdm       >=4.69.0    # 진행 표시
영역선택왜 이렇게 했나
학습 프레임워크PyTorch 2.13맥(MPS)·CUDA·CPU 자동 감지. 작은 모델이라 노트북에서도 학습 가능
패키지 매니저uvuv run python ... 한 줄로 재현. 빠르고 lock 파일(uv.lock)로 고정
데이터셋TinyStories작은 모델도 일관된 글을 배울 만큼 단순한 합성 동화 (~300MB 슬라이스)
토크나이저BPE (vocab 4096 / 32768)모든 실험이 같은 토크나이저 공유 — cross-entropy 비교가 성립하려면 필수
양자화그룹별 대칭 int4 PTQGGUF-Q4 스타일. group-128 ragged + fp16 scale로 16MB flash에 맞춤
추론(칩)순수 C 단일 헤더 llm.h동적 라이브러리·프레임워크 0. 호스트 검증과 칩 실행이 같은 코드
펌웨어 빌드Arduino ESP32 core 3.3.10arduino-cli로 컴파일·업로드, esptool로 모델 파티션 flash
용어 풀이
양자화 (Quantization) — int4 / int8
원래 파라미터는 32비트 실수(float32)로 저장되는데, 이걸 4비트 정수로 확 줄이면 용량이 1/8이 된다. 대신 값이 뭉툭해져 품질이 조금 떨어진다. 이 프로젝트의 핵심 발견 하나는, 그 거대한 lookup 표가 4비트로 압축해도 품질 손실이 유독 적다는 것 — 표는 원래 중복이 많아 뭉툭해져도 잘 버틴다. 그래서 flash에 4비트로 밀어 넣어도 이득이 유지된다(QAT 재학습 불필요).

4아키텍처 심화 분석 — 3단 메모리 회계와 PLE

핵심은 "파라미터를 접근 패턴에 따라 세 층으로 나눈다"는 것

이 프로젝트의 모든 것은 파라미터를 세 종류로 나누는 것에서 출발한다. 기준은 "얼마나 빠른가"가 아니라 "어떻게 접근되는가(access pattern)"다. 이 3단 분류가 칩의 3단 메모리(SRAM·PSRAM·flash)에 그대로 대응된다.

ESP32-S3 의 세 메모리 esp32-ai 의 세 파라미터 층 ┌──────────────────────────────────────────┐ ┌────────────────────────────────────────┐ │ SRAM 512KB · 초고속 · 극소 │ ← │ CORE (559K) 매 토큰 전부 계산에 쓰임 │ │ (내부 램, 가장 귀한 자원) │ │ → 반드시 빠른 SRAM 에 상주 │ ├──────────────────────────────────────────┤ ├────────────────────────────────────────┤ │ PSRAM 8MB · 중속 · 순차읽기 60.7MB/s │ ← │ STREAM(3.1M) 출력 head. 매 토큰 통째 │ │ (외부 램) │ │ 스캔하지만 처음→끝 순차 1회 │ ├──────────────────────────────────────────┤ ├────────────────────────────────────────┤ │ FLASH 16MB · 저속 · 랜덤 20.3us/512B │ ← │ TABLE (25M) 임베딩 표. 토큰마다 │ │ (거대하지만 느림) │ │ 딱 6줄(~450B)만 랜덤 읽기 │ └──────────────────────────────────────────┘ └────────────────────────────────────────┘ 28.9M 파라미터 = 559K(코어) + 3.1M(head) + 25M(표) · 4-bit 총 14.9MB 토큰당 표 비용 ~0.12ms(전체 메모리 시간의 0.7%) — 사실상 공짜

왼쪽이 하드웨어, 오른쪽이 모델이다. 셋을 짝지은 게 설계의 전부다. 여기서 제일 반직관적인 결정출력 head를 'core'에서 뺀 것이다. head는 매 토큰마다 전부 읽히지만 처음부터 끝까지 순차로 훑을 뿐이라, 용량이 아니라 대역폭만 잡아먹는다. 이걸 core에 넣어버리면 큰 어휘(vocab)가 감당 불가로 보이지만, 사실은 "느릴 뿐"이다. 이 구분이 큰 어휘 → 값싼 표라는 전체 전략을 가능하게 한다.

PLE(Per-Layer Embeddings)가 실제로 하는 일

PLE는 구글이 Gemma 3n / Gemma 4에서 쓴 기법이다. 보통 트랜스포머는 토큰을 입력에서 한 번만 임베딩하지만, PLE는 레이어마다 그 토큰 전용의 추가 정보를 주입한다. 그 "레이어별 추가 정보"가 바로 flash에 사는 거대한 표(ple_table)에서 온다. 토큰이 정해지면 각 레이어에 뿌릴 조각을 표에서 꺼내(랜덤 읽기 몇 줄), 레이어 내부에서 곱셈으로 게이팅한다.

// src/model.py — PLE 입력 조립 (설명용으로 단순화)
ple = ple_model_proj(x) * (d_model ** -0.5)        # ① 맥락 인지: 토큰 임베딩을 레이어별로 투영
ple = ple_proj_norm(ple.view(B,T, n_layers, ple_dim))
table = ple_table(idx) * (ple_dim ** 0.5)          # ② flash 표에서 토큰별 행을 꺼냄
ple = (ple + table) * (2 ** -0.5)                   # ③ 둘을 합쳐 각 레이어로 배분

// 각 레이어(Block) 안에서 게이팅 — 곱셈이라 서로를 억제할 수 있음
g = gelu(ple_gate(x))
x = x + ple_norm(ple_proj(g * ple_layer))          # 단순 편향이 아니라 '조건부' 주입
용어 풀이
Per-Layer Embeddings (PLE, 레이어별 임베딩)
토큰마다 레이어 수 × ple_dim 크기의 전용 표를 두고, 각 레이어에 그 토큰용 벡터를 하나씩 흘려 넣는 방식. 이 표는 "찾아보기(lookup)"만 하므로 계산이 없고, 토큰당 필요한 행만 읽으면 된다. 그래서 느린 flash에 둬도 비용이 거의 없다. 원래는 스마트폰의 RAM/저장 계층을 겨냥한 기법인데, esp32-ai는 이걸 세 자릿수 더 작은 마이크로컨트롤러의 SRAM/flash 계층에 처음 적용했다.

나머지 모델 본체는 요즘 LLM의 표준 부품으로 짜였다: 디코더-온리 트랜스포머, 정규화는 RMSNorm, 위치 인코딩은 RoPE(split-half), FFN은 SwiGLU, 입력·출력 임베딩은 묶음(tied). 배포 설정은 어휘 32768 · d_model 96 · 6개 레이어 · ple_dim 128이고, 계산 코어는 559K로 512KB SRAM에 들어가도록 크기를 맞췄다.

잘 만든 지점
"어휘가 클수록 이득이 커진다"는 반전

같은 구조인데 어휘 4096에서는 PLE 이득이 +0.025 nats로 미미하다. 그런데 어휘를 32768로 키우면 +0.098 nats(약 4배)로 뛴다. 큰 어휘는 표를 거대하면서 동시에 값싸게 만들기 때문이다(행은 많아지지만 토큰당 읽는 건 여전히 몇 줄). 즉 "표를 flash에 두는" 전제가 유리하게 작동하는 바로 그 구간에 배포 설정이 산다. 물리 제약과 알고리즘 이득의 방향이 같은 곳을 가리키는, 보기 드문 정합이다.

5디렉토리 구조 해부

학습(Python) · 검증(호스트 C) · 배포(칩 C)가 폴더로 갈린다

파일 수는 적지만(핵심 소스 ~1,800줄) 역할 분담이 또렷하다. src/는 파이토치 학습·양자화·export, firmware/는 C 추론과 온칩 배포, experiments/는 밤새 돌리는 실험 큐, data/는 데이터 준비다. "학습 → 양자화 → export → 호스트 검증 → 칩 flash"라는 파이프라인이 폴더 순서에 그대로 담겨 있다.

esp32-ai/ ├─ data/ │ └─ prepare.py TinyStories 다운로드 + BPE 학습 + uint16 토큰 bin 생성 ├─ src/ ★ 학습·양자화·검증 (PyTorch) │ ├─ model.py ★ TinyLM 정의 + 5개 실험 arm + 3단 파라미터 회계 (323줄) │ ├─ train.py 한 arm 학습, core 예산 맞춰 val loss 보고 (157줄) │ ├─ quantize.py 그룹별 int4 PTQ 후 품질 열화 측정 (139줄) │ ├─ export.py 학습 모델 → mmap 가능한 납작 바이너리 + golden logits (168줄) │ ├─ budget.py ★ 3단(core/stream/table) 메모리 회계 리포트 (99줄) │ ├─ analyze.py 런 결과(json) 집계·비교 (86줄) │ ├─ sample.py 학습된 모델로 텍스트 생성 (42줄) │ └─ gen_assets.py vocab.h 등 펌웨어용 에셋 생성 (53줄) ├─ firmware/ ★ 칩에서 도는 C 코드 │ ├─ common/llm.h ★ 포터블 단일 헤더 추론기 — 호스트=칩 동일 코드 (385줄) │ ├─ esp32_llm/ ESP32-S3 스케치 │ │ ├─ esp32_llm.ino 보드 진입점 · int8 head · dual-core 분할 (231줄) │ │ ├─ display.h ST7789 2.0" 240x320 화면 출력(선택) │ │ └─ partitions.csv factory 1MB + model 15MB 파티션 배치 │ ├─ host_verify/ PyTorch golden 대조 (verify.c · ppl.c) │ └─ bandwidth_bench/ 실제 칩의 SRAM/PSRAM/flash 대역폭 실측 ├─ experiments/ 밤샘 실험 큐 셸 스크립트(overnight.sh 등) ├─ media/ 데모 GIF ├─ RESULTS.md ★ 방법·ablation·온칩 실측 전체 기록 (13KB) ├─ pyproject.toml · uv.lock Python 3.12 · uv 잠금 └─ README.md 프로젝트 개요
읽는 순서 팁

처음 읽는다면 RESULTS.md(무엇을·왜 증명했나) → src/budget.py(3단 회계) → src/model.py(모델과 5개 arm) → firmware/common/llm.h(칩 추론) → RESULTS.md 온칩 섹션(성능 여정) 순서를 추천한다. model.py의 상단 주석은 다섯 실험 arm(baseline·ple·ple_notable·fatembed·bigcore)이 각각 무엇을 증명하려 존재하는지를 교재처럼 설명해 두어, 이 파일 하나만 읽어도 실험 설계 사고를 통째로 배울 수 있다.

6학습 포인트

이 저장소에서 실제로 배울 수 있는 것들

① 메모리 계층을 '아키텍처'로 다루는 법

가장 값진 교훈. 파라미터를 접근 패턴(dense-every-token / sequential-scan / sparse-lookup)으로 나누고, 각각을 맞는 메모리 층에 배치하는 사고방식이다. budget.py는 이 회계를 코드로 보여주고, "출력 head를 core에서 빼는" 한 줄의 결정이 어떻게 큰 어휘를 가능케 하는지 드러낸다. 임베디드가 아니어도, "무엇을 어디에 두는가"가 모델 설계의 일부라는 관점을 얻는다.

② Per-Layer Embeddings(PLE)를 코드로 이해하기

논문·블로그로만 접하던 Gemma의 PLE를, model.py60줄 남짓으로 직접 읽을 수 있다. "레이어별 표 lookup + 게이팅"이 실제 텐서 연산으로 어떻게 조립되는지, embed_scale(sqrt(ple_dim))처럼 문서에 안 적힌 결정적 디테일이 어디서 곱해지는지까지 주석으로 짚어 준다.

③ 정직한 ablation 실험 설계

"PLE가 좋다"를 어떻게 공정하게 증명하는가. 이 레포는 모든 arm의 계산 코어 크기를 이진 탐색으로 똑같이 맞추고(안 그러면 그냥 FFN 큰 놈이 이김), 2개 시드로 노이즈 대비 효과 크기를 재고, ple_notable(표 없는 PLE 배관)·fatembed(같은 표 예산을 입력에 몰아줌)·bigcore(표 예산을 코어 확장에)라는 대조군으로 "이득의 출처가 정확히 표"임을 격리한다. 실험을 설계하는 사람이라면 반드시 볼 예제다.

④ 엣지 양자화 — 무엇이 4비트를 잘 견디나

quantize.py는 그룹별 대칭 int4 PTQ를 구현하고, 거대한 표까지 포함해 전부 양자화한 뒤 품질을 다시 잰다. 배우는 것: 왜 작은 dense 모델은 4비트에 취약하고(모든 가중치가 중요), 중복 많은 큰 표는 4비트에 강한가. "flash 예산을 건 그 부분이 하필 가장 압축에 강하더라"는 결과가, 왜 QAT 없이도 되는지를 설명한다.

⑤ 포터블 C 추론 — 한 코드로 호스트와 칩 둘 다

firmware/common/llm.h(385줄)는 동적 아키텍처 없이 모델 헤더에서 차원을 읽어 동작하는 단일 헤더 추론기다. 같은 코드를 호스트에서 컴파일해 PyTorch golden과 대조(1e-5 일치)한 뒤, 글자 하나 안 바꾸고 ESP32로 컴파일한다. "칩에 올리기 전에 정확성을 완전히 못박는" 방법론의 정석이다.

⑥ 온칩 성능 엔지니어링 — 병목을 옮겨 가며 최적화

RESULTS.md의 온칩 섹션은 0.57 → 9.5 tok/s까지의 최적화 여정을 단계별 수치로 기록한다. head를 PSRAM에 int8로 미리 펼치고, 활성값도 int8로 양자화해 토큰당 언패킹을 없애고, 두 코어(LX7)에 출력 행을 나눈다. 결국 head가 계산이 아니라 PSRAM 대역폭에 묶인다는 걸 프로파일로 증명하고, "여기서 더 빨라지려면 SIMD가 아니라 읽는 바이트 자체를 줄여야 한다"는 결론에 도달한다. 실전 프로파일링 사고의 표본이다.

함정 주의
"58 tok/s"는 이론상 대역폭 천장이지 실측이 아니다

budget.py와 초기 추정은 대역폭만으로 ~58 tok/s가 가능하다고 봤지만, 실제로는 int4 언패킹과 수백만 번의 스칼라 float 연산이 대역폭보다 먼저 병목이 됐다. 실측 종단 속도는 ~9.5 tok/s다. 문서도 "58은 대역폭 천장일 뿐"이라고 반복해 못박는다. 벤치마크 숫자를 인용할 땐 이론 천장과 실측을 반드시 구분하자.

7하드웨어 / 시스템 요구사항

학습은 노트북으로, 실행은 8달러 보드로

칩 — ESP32-S3-DevKitC-1 N16R8 (실제 사용 보드)

항목사양 / 비고
CPUXtensa LX7 듀얼코어 @ 240MHz (출력 head를 두 코어로 분할)
내부 SRAM512KB — 가장 귀한 자원. 계산 코어(559K, 4bit 273KB)가 여기 상주
PSRAM(외부 램)8MB — head를 int8로 스테이징, 스크래치·KV 캐시. 순차 60.7MB/s
flash16MB — 25M 표를 memory-mapped(XIP)로 상주. 랜덤 20.3us/512B
화면(선택)ST7789 2.0" 240x320 (GMT020-02-7P) — 없으면 시리얼 출력만
가격$8

flash 파티션 배치 (partitions.csv)

# Name,   Type, SubType,  Offset,    Size
factory,  app,  factory,  0x10000,   0x100000   # 619KB 애플리케이션 (1MB 파티션)
model,    data, 0x40,     0x110000,  0xEE0000    # 14.9MB 모델 (15.6MB 커스텀 파티션)
# 모델 payload는 새로 export 할 때만 다시 flash. 펌웨어만 고치면 재업로드 불필요.

학습 쪽 요구사항

필요버전 / 비고
Python3.12 (.python-version) · uv 패키지 매니저 권장
PyTorch2.13+ — MPS(맥)·CUDA·CPU 자동 감지, 작은 모델이라 노트북도 가능
디스크TinyStories 슬라이스 ~300MB + 런 산출물(runs/*.json)
펌웨어 툴Arduino ESP32 core 3.3.10 · arduino-cli · esptool · C 컴파일러(호스트 검증용)

온칩 성능 — 최적화 단계별 실측 (200토큰 평균)

구현 단계종단 속도모델 스텝
첫 포터블 포트0.57 tok/s1,757 ms
PSRAM head + 스칼라 정리4.6–4.8 tok/s194 ms
듀얼코어 정확 head5.7–6.2 tok/s139 ms
int8 head + int8 활성값(현재)~9.5 tok/s103 ms

현재 런타임 프로파일(토큰당, 듀얼코어 wall): head 57.6 · attn 25.6 · PLE 8.5 · FFN 6.9 · 입력 4.4 ms. head가 토큰당 int8 가중치 2.43MB를 읽어 60.7MB/s에서 ~40ms 대역폭 바닥에 묶인다. 여기서 더 빨라지려면 SIMD가 아니라 읽는 바이트를 줄이는 방향(int4 head + SIMD 언패킹)이나 더 작은 head(모델 변경)가 필요하다.

8직접 해볼 수 있는 실습 과제

난이도별 5개 — 보드 없이도 4번까지 가능하다
과제 1 · 난이도 ★☆☆☆☆

budget.py로 3단 메모리 회계를 눈으로 보기

보드 없이 시작. uv run python src/budget.py --bits 4를 돌려, core/stream/table 각 층이 몇 KB인지, SRAM에 맞는지, 토큰당 표 읽기가 몇 ms인지를 출력으로 확인하라. 어휘·d_model·ple_dim을 바꾸며 "무엇이 SRAM을 넘치게 하는가"를 감으로 익히는 게 목표다.

과제 2 · 난이도 ★★☆☆☆

TinyStories 슬라이스 준비 + BPE 토크나이저 학습

uv run python data/prepare.py로 TinyStories 300MB를 받아 BPE를 학습하고 uint16 토큰 bin을 만든다. 왜 모든 실험이 같은 토크나이저를 공유해야 cross-entropy 비교가 성립하는지, vocab 4096과 32768이 bin 크기·이후 PLE 이득에 어떻게 영향을 주는지 prepare.py 주석과 함께 읽어라.

과제 3 · 난이도 ★★★☆☆

baseline vs ple 한 arm씩 학습해 val loss 비교

src/train.py --arm baseline--arm ple같은 target-core로 각각 학습해, PLE가 정말 낮은 val loss(=낮은 perplexity)를 내는지 재현하라. 노트북(MPS/CPU)이면 스텝 수를 줄여 감만 봐도 된다. make_model()FFN 크기를 이진 탐색해 코어를 맞추는 부분을 꼭 확인 — 이게 공정 비교의 핵심이다.

과제 4 · 난이도 ★★★★☆

4-bit PTQ로 이득이 살아남는지 재현

src/quantize.py로 학습 모델을 그룹별 int4로 양자화→역양자화한 뒤 val loss를 다시 재라. baseline과 ple가 각각 얼마나 열화되는지 비교해, "표가 4비트에 더 강하다(→ PLE 이득이 유지된다)"는 결론을 스스로 확인한다. 그런 다음 export.pymodel.bin과 golden을 뽑고, 호스트에서 verify.c를 컴파일해 C가 PyTorch와 1e-5로 일치하는지 대조하라.

과제 5 · 난이도 ★★★★★

실제 ESP32-S3 보드에 올려 온칩 생성 보기

보드가 있다면 최종 단계. arduino-cli compile(ESP32 core 3.3.10, -O3)로 펌웨어를 빌드하고, esptool write_flash 0x110000 model.bin으로 모델 파티션을 굽는다. 시리얼 모니터에서 "Once upon a time…"초당 ~9.5 토큰으로 이어지는지 확인하라. 부팅 진단(V=32768 D=96 L=6 H=4 F=66 P=128, head int8 2.53MB, PSRAM 여유 ~5100KB)이 문서와 일치하는지도 함께 본다.

9관련 기술 심화 학습 로드맵

4주 커리큘럼 — 이 저장소를 발판으로
주차주제할 것
1주차트랜스포머 기초model.py 정독 → RMSNorm·RoPE(split-half)·SwiGLU·tied embedding이 각각 무엇인지 이해 → sample.py로 작은 모델을 학습·생성해 감 잡기
2주차메모리 계층 + PLEbudget.py로 3단 회계 → model.py의 PLE arm 60줄 정독 → Gemma PLE 원리(레이어별 표 lookup + 게이팅)와 "어휘가 클수록 이득" 논리 정리
3주차양자화 + C 추론quantize.py의 그룹별 int4 PTQ → export.py 바이너리 포맷 → llm.h로 "호스트=칩 동일 코드"와 golden 대조(1e-5) 흐름 파악
4주차온칩 배포·최적화ESP32 툴체인(arduino-cli·esptool)·파티션 → bandwidth_bench로 대역폭 실측 → RESULTS.md 온칩 섹션으로 int8 head·듀얼코어·대역폭 병목 최적화 여정 재현
학습 순서 팁

임베디드가 낯설어도 1~3주차는 순수 소프트웨어라 노트북만으로 끝난다. 이 레포는 "작은 트랜스포머를 바닥부터 학습하고 → 양자화하고 → C로 이식하고 → 정확성을 못박는 전 과정"의 압축 교재다. 마이크로컨트롤러라는 소재를 걷어내도, LLM 추론 파이프라인을 처음부터 끝까지 손으로 짜 보고 싶은 사람에게 값지다. 보드(과제 5)는 여유가 될 때 얹으면 된다.

10핵심 키워드 사전

이 문서에 나온 용어 빠른 정리
용어
ESP32-S3Wi-Fi 내장 듀얼코어(LX7) 마이크로컨트롤러. 512KB SRAM·8MB PSRAM·16MB flash, 약 $8
PLE (Per-Layer Embeddings)토큰별로 레이어마다 전용 벡터를 주입하는 기법(Gemma). 표 lookup만 하므로 느린 flash에 둬도 값이 쌈
TinyStories작은 모델도 일관된 글을 배우도록 만든 단순 합성 동화 데이터셋 (Eldan & Li, MS Research)
core / stream / table파라미터 3층. core=매토큰 계산(SRAM), stream=출력 head 순차스캔(PSRAM), table=토큰당 몇 줄 lookup(flash)
SRAM / PSRAM / flash칩의 3단 메모리. 빠르고 작음 / 중간 / 느리고 큼. 각각 core·head·표를 맡음
양자화 · int4 / int8float32 가중치를 4/8비트 정수로 줄여 용량·대역폭 절약. 표는 4비트에 특히 강함
PTQPost-Training Quantization — 학습 후 양자화. QAT(양자화 인지 재학습) 없이도 이득 유지
perplexity (ppl) / nats언어모델 품질 지표. 낮을수록 좋음. nats는 자연로그 기준 손실 단위(값이 클수록 개선폭 큼)
RoPE / SwiGLU / RMSNorm현대 LLM 표준 부품 — 회전 위치 인코딩 / 게이트형 FFN / 경량 정규화
tied embedding입력 임베딩과 출력 head가 같은 가중치를 공유(묶음)해 파라미터 절약
memory-mapped / XIPflash를 RAM처럼 주소로 직접 읽기. 표를 통째로 옮기지 않고 필요한 행만 그 자리에서 읽음
ablation (대조 실험)요소를 하나씩 빼거나 바꿔(ple_notable·fatembed·bigcore) 이득의 진짜 출처를 격리하는 실험 설계
tok/s초당 생성 토큰 수. 이 프로젝트 종단 실측 ~9.5, 대역폭 이론 천장 ~58
duel-core / LX7ESP32-S3의 두 Xtensa LX7 코어. 출력 head 계산을 둘로 나눠 처리

11참고 링크

원문으로 더 파고들기