Unity 게임에 8.7M LLM을 올려봤다
인디 Unity 게임의 펭귄 DJ가 영어로 한마디씩 답하게 하고 싶었다. 그래서 세상에서 가장 작은 LLM 중 하나인 8.7M 파라미터짜리 모델을 게임 안에 올려봤다. 결론부터 말하면 어설프다. 근데 그래서 귀엽다.
어떤 모델인가
arman-bd/guppylm-9M을 썼다. MIT 라이선스, GPT-2 스타일 decoder, 6 layer × 6 head × 384 hidden, BPE vocab 4096, max sequence 128 tokens. 원작자가 “Build your own LLM in 5 minutes” 교육용으로 공개한 토이 모델이다. 이름엔 LLM이 붙어 있지만 GPT-4의 약 1/170,000 크기다.
| 모델 | 파라미터 | GuppyLM 대비 |
|---|---|---|
| GuppyLM | 8.7M | 1× |
| GPT-2 small | 124M | 14× |
| TinyLlama | 1.1B | 126× |
| Llama-3.2-1B | 1B | 115× |
| Phi-3-mini | 3.8B | 437× |
| GPT-3.5 | 175B | 20,000× |
| GPT-4 | ~1.5T | ~170,000× |
Sentis가 모델을 거부했다
Unity 공식 Sentis(현 InferenceEngine)에 ONNX를 그대로 넣으면 될 줄 알았다. 그런데 GuppyLM이 INT8 동적 양자화 모델이라 import 자체가 막혔다.
1
OnnxLayerImportException: Could not read tensor data for constant tensor.
결국 Microsoft ONNX Runtime의 native DLL을 NuGet에서 직접 받아 Unity Plugins 폴더에 수동으로 배치했다. Win64는 onnxruntime.dll 11MB, Mac arm64는 libonnxruntime.dylib 25MB.
토크나이저는 직접 짰다
ONNX 모델은 token ID로만 입력을 받는다. 텍스트 ↔ ID 변환은 별도로 처리해야 한다. 처음엔 Microsoft.ML.Tokenizers를 끌어오려 했는데 BCL.Memory, System.Text.Json, BCL.HashCode 같은 transitive 의존성이 Unity에서 dependency hell을 만들었다.
결국 HuggingFace tokenizer.json을 직접 파싱하는 ByteLevel BPE를 C#으로 자체 구현했다. GPT-2/Llama 계열의 표준 알고리즘이라 150줄 정도로 끝났다. 다른 ONNX LLM에서도 그대로 재사용 가능하다.
다양성은 Top-k sampling
같은 prompt에 매번 같은 답이 나오면 게임 채팅으론 재미없다. Top-k sampling (k=20, T=0.85)을 얹어 매번 살짝 다른 답이 나오도록 했다.
게임 안에서 펭귄 DJ에게 영어로 말을 걸면 8.7M LLM이 ONNX Runtime으로 한 토큰씩 답을 뱉는다. 상단 스샷이 그 순간이다.
어떻게 답하는가
같은 입력 "hello dj"를 다섯 번 반복했다.
1
2
3
4
5
1. "i'm it in terms of water or food."
2. "my don't know what i'm just a fish. i live from."
3. "i don't know i'm just a fish."
4. "i have the cold and then i'll be behind the castle."
5. "i don't know it i'm just a fish. i swim and eat."
다른 입력 "hello dj how are you".
1
"I'm just say brr. I'll be in the middle recovering."
문법은 깨지고, 의미는 단편적이고, 같은 패턴이 계속 반복된다. 학습 데이터에 박혀 있는 "i'm just a fish"가 거의 모든 입력에 끌려 나온다. 진짜 instruction following은 일어나지 않는다.
근데 여기서 우연이 하나 터졌다. 펭귄 DJ가 “물고기라서 잘 모른다”고 답하는 게 캐릭터에 묘하게 잘 어울린다. 어설픈 게 오히려 컨셉이 돼버렸다.
게임 본체의 캐릭터 다양화는 결정론적 코드로 푼다. Trait 8종 + Secondary 6종 + 효과 트리. LLM은 그 위에 얹는 분위기 레이어다. 의미 전달은 코드가 맡고, 우연한 매력은 LLM이 맡는 셈이다.
Fine-tune 시도와 롤백
도메인이 좁으니 펭귄·DJ 라인을 직접 만들어 fine-tune하면 메울 수 있지 않을까 싶었다. 일단 해봤다.
데이터는 159줄 — DJ 답변 50개, 펭귄 mumble 56개(8 trait × 7 state), 콜로니 일기 30개, 여왕 헌사 20개. CPU에서 80초 만에 학습이 끝났다.
1
2
3
Step 0: loss 10.15
Step 100: 3.92 / eval 4.34
Step 350: 2.14 / eval 3.33
loss는 잘 떨어졌다. 그런데 PyTorch → ONNX → INT8 양자화를 거쳐 추론해보니 오히려 더 망가졌다.
1
2
3
"theinings."
"in ones."
"the coled colereeng honenoned."
이유는 세 가지로 본다. 첫째, 159줄은 generalize 학습에 턱없이 부족하다(보통 1,000줄 이상은 필요). 둘째, 8.7M은 fine-tune으로 메울 수 있는 한계 자체가 좁다. 셋째, INT8 양자화의 quality 손실은 작은 모델일수록 더 치명적이다. 결국 base 모델로 롤백했다. 이 사이즈는 데이터를 대량으로 부어야 그나마 의미 있는 학습이 일어난다.
빌드는 얼마나 무거워졌나
| 항목 | 사이즈 |
|---|---|
| GuppyLM INT8 ONNX | 10MB |
| ONNX Runtime managed DLL | 0.2MB |
| Win64 native DLL | 11MB |
| Mac arm64 native DLL | 25MB |
| 총 추가 | ~46MB |
모델보다 inference engine이 더 크다. 10MB 모델 + 35MB 런타임. 이게 인디 게임에 LLM 하나 얹을 때 치러야 하는 현실적인 비용이다.
핵심 코드
C# wrapper의 핵심 함수 — 텍스트 → 토큰 → 추론 → 텍스트.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public string GenerateText(string prompt, int maxNewTokens = 12,
int eosTokenId = 2,
int topK = 20, float temperature = 0.85f) {
var inputIds = _tokenizer.Encode(prompt);
var working = new List<long>(inputIds.Count + maxNewTokens);
foreach (var i in inputIds) working.Add(i);
var generated = new List<int>();
for (int step = 0; step < maxNewTokens; step++) {
int next = PredictNextSampled(working, topK, temperature);
if (next < 0 || next == eosTokenId) break;
generated.Add(next);
working.Add(next);
}
return _tokenizer.Decode(generated).Trim();
}
ChatML prompt 형식. 작은 모델일수록 prompt 포맷을 학습 당시와 똑같이 맞춰야 한다.
1
2
3
4
5
<|im_start|>user
hello dj
<|im_end|>
<|im_start|>assistant
다음 빌드
8.7M은 분위기용으로만 두기로 했다. 의미 있는 응답은 직접 큐레이션한 fallback 라인 풀(80개+)이 메인이다. 제대로 된 LLM 응답은 1B+ 모델에서 보고 싶다. TinyLlama 1.1B(INT4 기준 +600MB) 또는 Phi-3-mini 3.8B(+1.5GB)로 옮겨갈 예정이다. 1GB 넘는 추가 용량이 게임의 매력에 실제로 기여하는지는 시연 끝나고 진지하게 따져봐야 한다.
다행히 다음 빌드는 모델만 갈아끼우면 된다. ONNX Runtime이랑 자체 BPE 토크나이저는 그대로 재사용. 인프라 작업은 이번에 한 번으로 끝났다.
8.7M짜리는 결국 instruction following이 안 되는, 이름만 LLM에 가까운 토이 모델이다. 의미 있는 대화는 1B+로 넘어가서 다시 본다.
