[기술 딥다이브] 폰에서 Laya, Jev와 붙였다
한 줄 요약: 421M 의사결정 모델 Laya를 갤럭시 S25에 직접 깔아 클라우드 Jev API와 같은 입력으로 비교했더니, 분류는 양쪽 다 맞췄지만 한국어 해지 판정(0.97 대 0.009)과 재현성에서 차이가 났다.
지난 릴스 해설에서 Laya라는 오픈소스 System 1 의사결정 모델을 소개했어요(421M 모델 이야기). 이번에는 글에서 멈추지 않고 실제로 해봤어요. 갤럭시 S25의 Termux에 Laya를 통째로 설치하고, 동일한 입력을 클라우드 API인 TypeSafe Jev에도 던져 답을 맞대어 봤어요. 이 글은 그 실험의 기록이에요. 어떤 구조로 답을 내는지, 안드로이드 폰에서 어떤 관문을 통과해야 하는지, 그리고 무엇이 양쪽을 가르는지 순서대로 볼게요.
답을 쓰지 않고 표시하는 모델
우리가 흔히 쓰는 LLM의 판정 방식과 Laya의 방식은 답을 내는 단위부터 달라요. LLM은 “정답은 billing입니다”라는 글을 토큰 하나씩 이어 붙여 쓰고, 그 문장을 코드가 다시 읽어(파싱) 부서를 정해요. Laya는 텍스트를 아예 생성하지 않아요. 상태와 질문을 한 번에 읽어 순전파(모델의 계산을 딱 한 번 통과하는 과정) 한 번으로 choice(보기 중 선택), score(서열 점수), noul(참일 확률)이라는 유형화된 답을 꺼내요. 아래 도식은 이 두 가지 경로를 나란히 보여줘요.
한 가지 주의할 것은 이 설계가 만능이 아니라는 점이다. 선택지가 수십 개가 넘어가면(Jev의 벤치마크에서 72개) Laya는 답안마다 쓸 수 있는 문장 길이를 똑같이 나눠 담는 구조라 선택지 하나에 몇 글자도 배정되지 않아 정확도가 떨어지고, 정답 하나만 가리키는 게 아니라 “이쪽도 이 확률”이라는 확률 분포 전체를 큰 모델과 얼마나 비슷하게 내놓는지 재는 지표에서도 아직 뒤처진다. 빠른 표시기와 느린 작가는 서로를 대체하지 않고 나뉜다.
폰에 까는 데 필요한 것들
Laya는 PyTorch 기반 파이썬 패키지라 관문은 모델이 아니라 실행 환경이에요. Termux는 리눅스 표준 바이너리(manylinux 휠)가 안 돌아가기 때문에, PyPI에서 pip install torch로는 시작조차 못 해요. 대신 Termux 사용자 저장소(TUR)가 안드로이드용 torch 2.11 빌드를 제공하고, safetensors만 소스 빌드(Rust)로 통과하면 돼요. 실제로 통과한 경로는 다음과 같아요.
pkg install -y python-torch python-tokenizers # TUR 빌드: torch 2.11, numpy 자동
pkg install -y rust # safetensors 소스 빌드용
pip install maturin && pip install safetensors # Rust 빌드 몇 분
pip install --no-deps laya transformers "huggingface_hub<1.0"
pip install filelock regex requests tqdm packaging pyyaml
여기서 두 개의 버전 함정이 나와요. 첫째, pip install laya를 그대로 치면 PyPI torch를 끌어오려다 실패하므로 --no-deps로 본체만 깔고 나머지를 손으로 채워야 해요. 둘째, transformers 5.x에서는 laya 0.3.4가 정상 동작하지 않아요. 처음에 최신 5.17로 돌렸더니 같은 입력에 신뢰도 0.04짜리 답이 나왔어요(README 기대치는 billing 0.94). 4.57로 내리니 정상이었는데, 이 버전은 huggingface_hub<1.0과 tokenizers<=0.23.0을 요구해서 hub는 0.36.2로 내리고 tokenizers는 apt 패키지(0.23.2)의 버전 검사를 한 줄 패치해 통과했어요. laya의 transformers>=4.45.0 요구만 믿으면 안 된다는 것이 첫 번째 교훈이에요.
모델 크기는 생각보다 작아요. 영어 421M 체크포인트가 fp16(가중치를 16비트 소수로 담는 형식)으로 842MB, 다국어 322M이 614MB라 릴스의 “~1GB 추론 메모리” 주장이 그대로 맞아떨어져요. 421M × 2바이트 = 842MB이니 산수로도 확인돼요.
스레드 수가 답을 흔든다
실험 도중 가장 뜻밖의 발견은 속도도 정확도도 아니고 재현성이었어요. 기본 설정(8스레드)으로 같은 프로세스 안에서 같은 입력을 세 번 연속 실행하면 답이 달라졌어요. 아래는 다국어 체크포인트에 한국어 한 줄을 넣고 돌린 실측이에요.
8스레드에서는 신뢰도가 0.30 → 0.72 → 0.13으로 요동쳤고, churn 확률도 0.008과 0.519 사이를 오갔어요. torch.set_num_threads(1)로 고정하자 세 번 모두 (billing, conf 1.0, churn 0.009)로 완전히 동일해졌고, 이때의 값이 README의 GPU 기대치와 가장 가까웠어요. 원인은 병렬 계산 라이브러리(OpenBLAS)의 덧셈 순서예요. 여러 스레드가 나눠 더할 때 더하는 순서가 배분에 따라 달라지면 소수점 합계가 마지막 자리에서 미세하게 달라지는데, 이 모델은 그 미세한 차이가 답을 정하는 내부 점수(로짓)를 크게 뒤집을 만큼 예민했어요. “보정된 신뢰도”를 파는 모델이 플랫폼에서 재현되지 않으면 신뢰도가 신뢰가 못 되는 일이기에, 이 발견이 이번 실험의 가장 값진 수확이에요.
같은 질문을 Jev에도 던지다
비교 상대는 TypeSafe의 Jev(jev-1.13.0)로 정했어요. 이 블로그의 발행 전 검수 도구가 바로 이 모델을 쓰기 때문에, 같은 질문 체계(choice·score·noul)를 공유해요. 동일한 두 상태(영어 이메일, 한국어 텍스트)에 동일한 세 질문을 양쪽에 던졌어요. Laya는 S25 CPU에서 표준 정밀도(fp32, 소수점을 32비트로 담는 형식), 1스레드로 돌렸고 Jev는 API로 실행했어요.
| 항목 | Laya 영어 421M | Jev jev-1.13.0 | Laya 다국어 322M | Jev jev-1.13.0 |
|---|---|---|---|---|
| department | billing (0.86) | billing (1.0) | billing (1.00) | billing (1.0) |
| urgency | 1.44 / 2 | 1.87 (conf 0.80) | 1.79 / 2 | 1.85 (conf 0.77) |
| churn_risk | 0.825 | 0.97 | 0.009 ⚠️ | 0.97 |
| 지연 | 2.6초 / 3질문 | 791ms / 6질문·2상태 | 0.8~3초 | (같은 호출) |
| 비용 | $0 | 947토큰 ≈ $0.00004 | $0 | (같은 호출) |
해석은 세 가지예요. 첫째, 분류는 양쪽 다 정답이었고 urgency도 큰 틀에서 일치했어요(Jev의 1.87이 Laya의 GPU 공식 기대치 1.84에 더 가까워요). 둘째, 결정적 격차는 churn이었어요. “해지하겠습니다”라는 한국어 문장에서 Jev는 0.97을 뽑았지만 Laya 다국어 체크포인트는 0.009로 완전히 놓쳤어요. 영어에서는 Laya도 0.825로 잘 잡았기에, 이는 다국어 체크포인트의 한국어 churn 판정이 아직 미숙하다는 신호예요. 셋째, 지연 순서가 직관과 반대였어요. 네트워크 왕복을 포함한 API(791ms, 질문 여섯 개)가 폰 CPU(2.6초)보다 빨랐어요. 릴스의 “cloud가 기다리는 동안 local이 결정한다”는 슬로건은 애플 실리콘의 MLX가 있어서 성립하는 문장이지, 범용 안드로이드 CPU에서는 아직 그렇지 않아요.
확률의 질감도 달랐어요. Jev는 billing에 1.0, 나머지에 0.0을 주는 극단 분포인데, laya 저장소가 Jev를 비판하며 든 지점(확률 분포가 너무 뾰족하다)이 그대로 관찰됐어요. Laya는 0.86처럼 완만한 분포를 내놓아요. 어느 쪽이 맞냐는 용도의 문제예요. 자동 분기에는 뾰족한 게 편하고, 불확실성을 다뤄야 하는 파이프라인엔 완만한 게 나아요.
로컬 System 1의 자리
이 실험으로 그려지는 그림은 계층이에요. 반복되는 유형화된 판정(부서 분류, 위험도, 스팸 여부)은 로컬 System 1이 밀리초~초 단위로 처리하고, 모호하거나 선택지가 많은 것만 프론티어 모델로 보내요. laya 저장소도 이 협업 구조를 정식 워크플로 프리셋(요청을 어느 모델로 보낼지 고르는 라우팅, 탈옥·주입 공격 차단, 고객 문의 분류)으로 제공해요. 다만 두 가지 조건이 붙어요. 로컬 CPU 배포에서는 스레드 수를 고정해 재현성을 확보할 것, 그리고 다국어 체크포인트의 한국어 판정은 자기 도메인 데이터로 미세조정한 뒤 믿을 것(Kaggle 무료 T4 두 장으로 4~5시간이면 된다고 저장소가 안내해요).
설치 스크립트와 비교 스크립트 전문은 아래 gist에 공개했어요. Laya를 폰에서 돌리려는 사람에게 이 글의 요약은 한 문장이에요. 돌아가지만, 돌리기 전에 스레드를 하나로 고정하고 한국어 성능은 미세조정으로 확인하세요.
출처
- 직접 실험 기록 gist, “Laya를 Termux(안드로이드)에서 돌리기” (2026-09-21): https://gist.github.com/sng2c/001ea5fafe2822cc910ecd55b8e61816
- GitHub, NandhaKishorM/laya 저장소와 BENCHMARKS 문서: https://github.com/NandhaKishorM/laya
- Hugging Face, convaiinnovations/laya 모델 카드: https://huggingface.co/convaiinnovations/laya
- 이 블로그 선행 글, “33ms에 답하는 로컬 의사결정 모델 Laya”: https://pulmap.app/blog/reel-laya/
- Termux 사용자 저장소(TUR)의 python-torch 패키지: https://github.com/termux-user-repository/tur