메뉴
HN
Hacker News • 56일 전

1975년산 8비트 6502 프로세서에서 구동하는 자회귀 언어모델

IMP
7/10
핵심 요약

현대의 머신러닝 기술을 1975년형 8비트 6502 프로세서(RAM 32KB)에 탑재하여 자회귀 언어모델을 구동시킨 흥미로운 프로젝트입니다. 가중치 양자화에 CPU 연산에 유리한 BitNet 구조를 적용하고, 코드를 극한으로 최적화하여 구형 하드웨어에서도 AI 모델의 텍스트 생성이 가능함을 증명했습니다. 제한된 하드웨어 환경에서 모델을 경량화하고 최적화하는 실용적인 접근법을 보여줍니다.

번역된 본문

요약: 저는 Mamba 기반의 아주 작은 자회귀 언어 모델을 학습시키고, 이것을 8비트 6502 프로세서(1975년 출시, RAM 32KB)에서 실행하기 위한 추론 엔진을 작성했습니다. 아버지의 BBC Micro 컴퓨터에서 이 모델을 실행시켜 다음과 같은 텍스트를 생성했습니다.

once upon a time tom and lily saw things lily were sad her house he heartd them ilily and tom said yes she saw a little girl smiled tom was so excited her mom said yes

MOS 6502는 1975년에 출시된 8비트 마이크로프로세서로, BBC Micro와 Apple II 등에 탑재되었습니다. 저는 80년대식 아버지의 BBC Model B 컴퓨터를 사용할 수 있게 되었습니다. 현대의 머신러닝 기술을 이용하여 이 기계에 탑재할 수 있는 가장 강력한 언어 모델이 어떤 것일지 확인해 보고 싶었습니다.

놀랍지 않게도 이 작업에는 상당한 도전 과제가 따랐습니다. 모델 가중치와 추론 코드는 25KB의 사용자 메모리 공간 내에 포함되어야 했습니다. 최종 설정에서는 추론 코드에 9KB, 모델 가중치에 13KB를 할당했습니다. 이 CPU는 8비트 정수 데이터 유형으로만 작동하며, 명령어 집합에 곱셈 연산이 포함되어 있지 않습니다.

추론 코드에는 CC65를 사용하여 C 언어 코드를 6502 명령어 집합으로 컴파일했습니다. 맥북에서 학습시킨 모델 바이너리는 PlayUEF와 직접 제작한 3.5mm-테이프 변환 케이블을 사용하여 BBC Micro로 전송했습니다. 노트북이 이어폰 단자를 통해 오디오를 재생하면, BBC Micro는 마치 테이프 드라이브를 읽고 있다고 인식하게 됩니다.

sim65 에뮬레이터를 통해 C 언어 추론 바이너리와 기준이 되는 파이썬 모델 구현 간의 패리티(동등성) 검사를 수행했습니다. 또한 전체 추론 엔진은 BBC Micro에서 실제로 구동하기 전에 jsbeeb 에뮬레이터에서 먼저 테스트할 수 있습니다.

여러분도 직접 실행해 볼 수 있습니다. 아래 링크를 클릭하면 웹 브라우저에서 BBC Micro를 부팅하고, GitHub에서 UEF 테이프 이미지를 곧바로 불러온 뒤 모델을 실행할 명령어를 자동으로 입력합니다. 에뮬레이터를 설치할 필요가 없습니다 (단, 텍스트 생성에는 몇 분 정도 소요됨).

BBC Micro에서 BitNet 구동하기 →

모델링

이 프로젝트의 목표는 최첨단 언어 모델들과 유사하게 토큰을 하나씩 생성해 내는 자회귀 언어 모델을 구축하는 것입니다. 이 모델은 기존 문맥으로부터 다음 토큰을 생성해내는 함수 $f$입니다: $$ f: \text{'the cat sat on the ma'} \mapsto \text{'t'} $$

대규모 언어 모델에서 '토큰'은 일반적으로 단어나 하위 단어 단위를 의미합니다. 하지만 이 글에서 사용한 어휘 집합(토큰 목록)은 26개의 알파벳과 공백(' ') 문자입니다. 이처럼 소규모 모델에서는 더 큰 어휘 집합(예: 단어 또는 하위 단어)을 사용할 경우, 어휘 인코더/디코더 계층이 파라미터 예산을 너무 많이 소모하게 됩니다.

임베딩 계층은 토큰을 모델의 은닉 차원(우리의 경우 56차원)으로 매핑합니다. 토큰의 공간적 임베딩이 어떻게 작동하는지 이해하려면 3Blue1Brown의 신경망 관련 영상이 큰 도움이 됩니다. $$ g: {\text{a}, \text{b}, ... , \text{z}, '\text{ }'} \to \mathbb R^{56} $$

토큰이 고차원 공간에 매핑되면, 토큰 간의 순환적 종속성을 모델링하기 위해 혼합 계층이 사용됩니다 (순환 계층 참조).

BitNet

BitNet은 CPU에서 빠른 추론을 가능하게 하는 방법으로 소개되었습니다. 행렬 곱셈 $Y = XW$은 $X$의 행과 $W$의 열 간의 내점(Dot product) 연산 집합입니다: $$Y_{ij} = \sum_k X_{ik} W_{kj}$$

BitNet은 가중치 $W$를 양자화하여 그 값이 삼진법 집합인 ${-1, 0, 1}$에 속하도록 만듭니다. 이렇게 하면 내적 연산이 단순한 덧셈과 뺄셈 연산의 시퀀스로 줄어듭니다: $$ \begin{align*} Y_{ij} &= X_{i1} W_{1j} + X_{i2} W_{2j} + \cdots + X_{in} W_{nj} \ &= X_{i1} - X_{i2} + ... - X_{in}\ \end{align*} $$

6502 프로세서의 명령어 집어에는 곱셈이 없습니다. 대신, 곱셈은 비트 시프트 및 덧셈 연산을 반복하여 수행됩니다. 단일 8x8 곱셈 및 누적 연산은 약 150 클록 사이클을 소모합니다. 반면 삼진법 누적 연산은 30 클록 사이클만을 소모하므로, 양자화된 가중치를 사용할 때 추론 속도를 훨씬 더 빠르게 만들어 줍니다.

int8이 8비트, float32가 32비트를 사용하는 것과 비교하여, BitNet 파라미터 각각은 $\log_2(3) = 1.58$ 비트의 저장 공간만 차지합니다. 우리는 1바이트당 4~5개의 파라미터를 압축하여 패킹할 수 있습니다. 1바이트당 5개의 파라미터를 넣는 것이 데이터 효율성 면에서는 더 좋지만, 이를 삼진법 값으로 언패킹하려면 반복적인 정수 나눗셈(floor-div) 연산이 필요하게 됩니다.

원문 보기
원문 보기 (영어)
tl;dr - I trained a tiny Mamba-based autoregressive language model and wrote an inference engine to run it on the 8-bit 6502 processor (from 1975, with 32KB RAM). Running it on my dad's BBC Micro generated the text below. once upon a time tom and lily saw things lily were sad her house he heartd them ilily and tom said yes she saw a little girl smiled tom was so excited her mom said yes The MOS 6502 is an 8-bit microprocessor released in 1975, powering the BBC Micro and the Apple II . I am lucky enough to have access to my dad's BBC Model B from the 80s; I wanted to see, using modern machine learning, what the strongest language model we could fit on this machine was. Unsurprisingly, this poses significant challenges. The model weights and inference code need to be contained within 25KB of user-space memory — my final configuration was 9KB inference code and 13KB model weights. The CPU only operates on an 8-bit integer datatype, and doesn't include multiplication in its instruction set. CC65 is used for the inference code, enabling compilation of C to the 6502 instruction set. A binary of a model trained on my MacBook can then be written to the BBC Micro using PlayUEF and a custom 3.5mm-to-tape cable I DIY'ed. This convinces the BBC that it's listening to a tape drive, while my laptop plays audio out of its headphone jack. The sim65 emulator allows a parity check between the C inference binary and the reference Python model implementation. The full inference engine can be tested on the jsbeeb emulator before running on the BBC Micro. You can run it yourself — the link below boots a BBC Micro in your browser, loads the UEF tape image straight from GitHub, and auto-types the commands to run the model. No emulator install required (note that generation takes a few minutes). Run BitNet on a BBC Micro → Modelling The goal of this project is to build an autoregressive language model — a language model that produces tokens one-by-one, similar to frontier language models. The model is a function $f$ that produces the next token from the existing context: $$ f: \text{'the cat sat on the ma'} \mapsto \text{'t'} $$ In large-scale language modeling, a 'token' would be a word or sub-word part. For this post, the vocabulary (list of tokens) used will be 26 letters plus the ' ' character. For models on this small scale, a larger vocabulary (eg. word or subword vocab) would lead to the vocabulary encoder / decoder layers consuming too much of the parameter budget. An embedding layer maps tokens into the hidden dimension (dim=56 in our case) of the model. 3Blue1Brown's video on neural networks is great for understanding how spatial token embeddings work. $$ g: \{\text{a}, \text{b}, ... , \text{z}, '\text{ }'\} \to \mathbb R^{56} $$ Once tokens are mapped to our high dimensional space, mixing layers are used to model recurrent dependencies between tokens (see recurrent layers ). BitNet BitNet was introduced as a method for fast inference on CPU. A matrix multiplication $Y = XW$ is a set of dot products of rows of $X$ with columns of $W$: $$Y_{ij} = \sum_k X_{ik} W_{kj}$$ BitNet quantizes $W$ such that its values lie in the ternary set $\{-1, 0, 1\}$. This reduces the dot product to a sequence of add/subtract operations: $$ \begin{align*} Y_{ij} &= X_{i1} W_{1j} + X_{i2} W_{2j} + \cdots + X_{in} W_{nj} \\ &= X_{i1} - X_{i2} + ... - X_{in}\\ \end{align*} $$ The 6502 processor's instruction set doesn't contain multiply: instead, a multiply is built from repeated bit-shift-and-add operations. A single 8×8 multiply and accumulate would cost 150 clock cycles. By contrast a ternary accumulate would take 30 clock cycles, making inference much faster with higher-quantization weights. Each BitNet parameter takes only $\log_2(3) = 1.58$ bits of storage, compared with 8 bits for int8, 32 for float32, etc. We can pack 4 or 5 parameters per byte: 5 parameters per byte is more data-efficient, but unpacking into ternary values requires repeated floor-divide-by-3 operations — not a native instruction on the 6502, making unpacking costly. By contrast, packing 4 parameters per byte assigns a chunk of 2 bits to each parameter, and unpacking just requires a right-shift. We opt for 4 parameters per byte for inference speed. In 13KB at 4 parameters per byte, 52k BitNet parameters can be stored. Experimental validation shows that the higher parameter count at lower quantization provides better bang-for-buck than fewer high-precision parameters. To train BitNet parameters, a similar method to other quantized training is used. The parameter is stored in full float32 precision, quantized to ternary during the forward pass, but gradients flow at full precision in the backward pass: def ternary_quantize(w: torch.Tensor) -> torch.Tensor: """Round to {-1, 0, +1} with straight-through estimator on the backward pass.""" q = torch.clamp(torch.round(w), -1.0, 1.0) # quantize forwards return w + (q - w).detach() # full precision gradient for backwards pass In practice the final LM head is kept in int4 — the output projection needs more resolution to spread probability across the vocabulary cleanly. Every other matrix parameter in the model is ternary. Recurrent Layer Architecture Attention Traditional language models such as GPT-3 used attention for modeling sequential relationships. Attention assigns vectors to each token, and uses a dot-product between each pair of vectors to allow tokens to exchange information. With a dimension of $d$ per token and a total of $s$ tokens in context, a dot product is required between our token and every token in the context window: $O(ds)$ FLOPs total. The inference forward pass (what we'll actually run on the BBC Micro) changes with context size. This is challenging with the 6502, as tensor sizes increase during inference, growing memory usage as we generate tokens. As transformers generate tokens, the KV cache grows by $O(\text{n layer} \times \text{hidden dim})$ per token generated. With our 32KB RAM budget, this would eat up the space we would prefer to use for storing model weights. What is the benefit that attention provides? It allows exact recall from each token; this allows transformers to perform well at copying and needle-in-a-haystack problems. But do we need this level of precision? For our dataset we just need enough short-term recall to learn how to spell words and produce simple grammar forms. Recurrent Models Other architectures have the property that each model forward pass has identical computation shape. State stored by the model is contained within a fixed-size state vector $h$: $$f: (t_i, h) \to t_{i+1}$$ where $t_i$ is token number $i$, and $h$ is a fixed size model hidden state. Both SSMs (eg. S4 , Mamba ) and RNN-style models ( GRUs , LSTMs ) satisfy this property, making them much more suitable for constrained-memory inference on the 6502. Why Not GRU? Recurrent models are known to be victim to the vanishing/exploding gradient problem , often leading to unstable training. Each step of the recurrence compounds errors by the magnitude of the largest eigenvalue of the forward matrix. An eigenvalue slightly larger than 1 can be catastrophic: as sequence length $s$ increases, errors compound as $\lambda^s, \lambda = 1 + \varepsilon$. In the BitNet regime, the spectral radius of our forward matrices typically far exceeds 1: in order to have a spectral radius of $\approx 1$, we would require that $98\%$ of weights are $0$. These instabilities from the GRU mean that we see divergence in every training run. The only way to avoid these is to store the primary weight matrix in a higher quantization (eg. int4). Since these are the largest matrices of the model, storing weights in int4 massively reduces the possible dimension of the model. In contrast, Mamba performs a per-channel scalar update, where the decay value per step is computed at inference-time (in the range $[0, 128) / 128$), and so by construction never exceeds 1, making explosion imp