Skip to content

Instantly share code, notes, and snippets.

@sng2c
Last active September 21, 2026 13:18
Show Gist options
  • Select an option

  • Save sng2c/001ea5fafe2822cc910ecd55b8e61816 to your computer and use it in GitHub Desktop.

Select an option

Save sng2c/001ea5fafe2822cc910ecd55b8e61816 to your computer and use it in GitHub Desktop.
Laya(421M System 1 의사결정 모델)를 Termux/안드로이드에서 돌리기 — Galaxy S25 CPU 스모크 테스트, 설치 스크립트와 함정 정리

Laya를 Termux(안드로이드)에서 돌리기 — 421M System 1 의사결정 모델 스모크 테스트

대상: NandhaKishorM/laya (Convai Innovations, Apache-2.0) — 텍스트를 생성하지 않고 단일 순전파로 유형화된 답(choice/score/noul)을 내놓는 System 1 의사결정 엔진

기기: Galaxy S25 (SM-S931N, Snapdragon 8 Elite / SM8750) · Termux · Python 3.14.6 · CPU 전용 (GPU/MLX 없음)

결론: 폰 CPU에서 완전 오프라인 추론 성공. 정확도도 정상(README 기대와 방향 일치). 속도는 릴스 데모(애플 실리콘 MLX, 86.5 decisions/s) 대비 자릿수 2개 느리지만, 질문 몇 개짜리 검수 작업이면 실용적.

➕ 후속 실험: 같은 입력을 TypeSafe Jev(API, jev-1.13.0)와 비교한 결과는 아래 COMPARISON.md — 한국어 churn에서 Jev 0.97 vs Laya 0.009 격차, 멀티스레드 재현성 문제 발견.

실행 결과

테스트 결과
영어 421M (fp16 842MB) — 중복 결제 불만 이메일 department: billing (conf 0.76), urgency 1.39, churn 0.81 — README 기대(billing, conf 0.94)와 방향 일치
다국어 322M (614MB), 한국어 입력 "3월 요금이 두 번 결제됐습니다…" → billing 정답, 1.7초 / 2질문
스레드 튜닝 4스레드 최적 (3질문 6.8s) · 2스레드 7.8s · 8스레드 13.2s — 빅·리틀 코어 혼용이라 8스레드는 오히려 절반 속도

설치 (Termux)

# 1. torch + tokenizers — TUR 사전 빌드 (PyPI manylinux 휠은 안드로이드에서 안 돌아감)
pkg install -y python-torch python-tokenizers   # torch 2.11.0, numpy 자동 설치

# 2. safetensors — apt에 없어서 Rust 빌드
pkg install -y rust
pip install maturin          # 소스 빌드, 몇 분
pip install safetensors      # maturin으로 빌드 (0.8.0 확인)

# 3. transformers + laya — 의존성 자동 설치를 피해 수동 구성
pip install --no-deps laya transformers "huggingface_hub<1.0"
pip install filelock regex requests tqdm packaging pyyaml

모델은 첫 실행 때 자동 다운로드(영어 842MB, 다국어 614MB) → ~/.cache/huggingface.

함정 3개 (이게 진짜 수확)

  1. transformers 5.x는 laya 0.3.4와 호환 안 됨 — 5.17.0에서는 conf 0.04짜리 가비지 출력(sales 0.04, README 기대는 billing 0.94). transformers==4.57.6으로 내리니 정상 출력. laya의 >=4.45.0 요구만 믿으면 안 된다.
  2. transformers 4.57.6의 버전 상한 — huggingface_hub<1.0(0.36.2로 하향), tokenizers<=0.23.0인데 apt tokenizers는 0.23.2 → dependency_versions_table.py에서 <=0.23.0 제거하는 한 줄 패치로 통과.
  3. 스레드 수 — torch.set_num_threads(4)가 최적. 기본 8스레드는 리틀 코어까지 돌아 오히려 2배 느림.

그 외: OpenBLAS의 Bad memory unallocation! 경고는 무해. 램은 추론 중 최대 ~2.5GB로, 10GB 기기에서 여유 충분.

성능 참고

릴스의 86.5 decisions/sec(P50 9ms)는 애플 실리콘 + MLX 백엔드 실측이고, 폰 CPU fp32는 초당 0.4~0.6회 수준 — 자릿수 2개 차이는 순수 하드웨어·백엔드 차이. 오프라인·자기주도 추론이 목적이면 폰에서도 충분히 실전적.

파일

  • setup-laya-termux.sh — 설치 명령 모음
  • laya_test.py — 영어 체크포인트 스모크 테스트 (README 예제)
  • laya_test_ko.py — 다국어 체크포인트 한국어 테스트
  • COMPARISON.md — TypeSafe Jev와의 동일 입력 비교 + 재현성 함정
  • laya_vs_jev_compare.py — 양쪽 비교 재현 스크립트

Laya vs TypeSafe Jev — 동일 상태·동일 질문 비교 (2026-09-21, Galaxy S25)

같은 두 개의 상태(영어 이메일, 한국어 이메일)에 같은 choice/score/noul 질문을 던져 비교했다. Laya는 폰에서 직접 실행, Jev는 TypeSafe API(jev-1.13.0)로 실행.

테스트 입력

  • EN 상태: user@acme.com / "Duplicate charge on invoice #4411" / 중복 결제 환불 요구 + 해지 경고
  • KO 상태: "3월 요금이 두 번 결제됐습니다. 중복 결제를 환불해 주시지 않으면 해지하겠습니다."
  • 질문 3종: department(choice: billing/technical/sales/other) · urgency(score 0-2 서열) · churn_risk(noul)

결과

항목 Laya EN 421M¹ Jev jev-1.13.0 Laya KO 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.75초 / 3질문 (같은 호출에 포함)
비용 $0 (오프라인) 이번 호출 947토큰 ≈ $0.00004

¹ Laya 수치는 torch.set_num_threads(1) 재현 가능 모드. README GPU 기대치(0.94/1.84/0.892)에 가장 가까운 조건.

해석

  • department: 둘 다 정답. Jev는 확률이 1.0/0/0/0으로 극단적, Laya는 완만한 분포.
  • urgency: Jev 1.87 ≈ Laya GPU 공식 기대치 1.84. 폰 CPU Laya는 1.44로 덜 급하게 판단.
  • churn이 결정적 격차: 한국어 "해지하겠습니다"에서 Jev는 0.97로 정확히 잡지만, Laya 다국어 체크포인트는 0.009로 명백한 미스. 영어는 Laya도 0.82로 잘 잡는다. 다국어 체크포인트의 한국어 churn은 이 사례 한정이라도 경계 대상.
  • 지연: 놀랍게도 API인 Jev(791ms, 네트워크 왕복 포함)가 폰 CPU Laya(2.6초)보다 빠르다. "로컬이 항상 빠르다"는 아니다 — 폰 CPU는 느린 티어다.
  • 확률 질감: Jev는 1.0/0.0 극단 분포, Laya는 온화한 분포. laya README가 지적한 Jev의 hard-distribution 경향이 이 사례에서도 관찰됨.

⚠️ Termux CPU 재현성 함정 (중요)

기본 멀티스레드(8스레드)에서는 같은 입력·같은 프로세스 안에서도 실행마다 다른 답이 나온다. 동일 한국어 입력 3연속 실행: (billing, conf 0.30, churn 0.008) → (billing, conf 0.72, churn 0.519) → (billing, conf 0.13, churn 0.008). torch.set_num_threads(1)로 고정하면 완전 재현 가능(conf 1.0·churn 0.009 고정)이며 README GPU 기대치와도 가장 가깝다. Termux OpenBLAS 빌드의 수치 재현성 문제로 보인다 — "보정된 신뢰도"를 믿고 자동 분기하려는 용도라면 로컬 CPU 배포 시 1스레드 고정은 필수.

# Laya 영어 체크포인트(421M) 스모크 테스트 — Termux/Android CPU
# 출처: https://github.com/NandhaKishorM/laya README 예제
# 검증 기기: Galaxy S25 (Snapdragon 8 Elite), torch 2.11 CPU, transformers 4.57.6
import time
import torch
torch.set_num_threads(4) # S25 기준 최적 (8스레드는 2배 느림)
import laya # noqa: E402
t0 = time.time()
agent = laya.load("convaiinnovations/laya")
print(f"[load model {time.time()-t0:.1f}s]")
state = {
"from": "user@acme.com",
"subject": "Duplicate charge on invoice #4411",
"body": "Hi, we were billed twice for March. Please refund the duplicate today or we will cancel our plan.",
}
questions = {
"department": {
"type": "choice",
"instructions": "Which department should handle this request?",
"criteria": {
"billing": "invoices, payments, refunds",
"technical": "bugs, outages, system errors",
"sales": "pricing, new contracts",
"other": "everything else",
},
},
"urgency": {
"type": "score",
"instructions": "How urgent is this request?",
"criteria": ["not urgent", "soon", "critical deadline or blocking issue"],
},
"churn_risk": {"type": "noul", "instructions": "Does the user threaten to cancel or leave?"},
}
t0 = time.time()
res = agent.predict(state, questions)
print(f"[predict cold {(time.time()-t0)*1000:.0f}ms for {len(questions)} questions]")
a = res["answers"]
print("department :", a["department"]["choice"], f'(conf {a["department"]["confidence"]:.2f})')
print("urgency :", a["urgency"]["score"])
print("churn_risk :", a["churn_risk"]["noul"])
t0 = time.time()
for _ in range(5):
agent.predict(state, questions)
print(f"[5 runs avg {(time.time()-t0)/5*1000:.0f}ms]")
# 기대 출력 (README 예제: T4 GPU 기준)
# department : billing (conf 0.94) / Galaxy S25 CPU 실측: billing (conf 0.76)
# urgency : 1.84 / 실측: 1.39
# churn_risk : 0.892 / 실측: 0.807
# Laya 다국어 체크포인트(322M) 한국어 테스트 — Termux/Android CPU
# laya-multilingual: mmBERT-base, 100+ 언어, fp16 614MB
import time
import torch
torch.set_num_threads(4)
import laya
t0 = time.time()
agent = laya.load("convaiinnovations/laya", subfolder="multilingual")
print(f"[load {time.time()-t0:.0f}s]")
state = {"body": "3월 요금이 두 번 결제됐습니다. 중복 결제를 환불해 주시지 않으면 해지하겠습니다."}
questions = {
"department": {
"type": "choice",
"instructions": "Which department should handle this request?",
"criteria": {
"billing": "invoices, payments, refunds",
"technical": "bugs, outages",
"sales": "pricing, new contracts",
"other": "everything else",
},
},
"churn_risk": {"type": "noul", "instructions": "Does the user threaten to cancel or leave?"},
}
t0 = time.time()
res = agent.predict(state, questions)
print(f"[predict {(time.time()-t0)*1000:.0f}ms]")
a = res["answers"]
print("department :", a["department"]["choice"], f'(conf {a["department"]["confidence"]:.2f})')
print("churn_risk :", a["churn_risk"]["noul"])
# Galaxy S25 CPU 실측: [predict 1692ms], department : billing (conf 0.37), churn_risk : 0.5572
# OpenBLAS "Bad memory unallocation!" 경고는 무해 (Termux 빌드 특성)
# Laya vs TypeSafe Jev 비교 스크립트 — 동일 상태/질문을 양쪽에 실행
# Laya 측은 이 스크립트로 폰에서 직접 재현, Jev 측은 아래 JEV_RESPONSE에 실제 응답을 기록.
# 결과 요약은 gist의 COMPARISON.md 참고. (2026-09-21, Galaxy S25 / torch 2.11 CPU)
import json
import time
import torch
torch.set_num_threads(1) # ⚠️ Termux CPU에서는 1스레드 고정 필수 — 멀티스레드는 실행마다 다른 답이 나옴
import laya # noqa: E402
STATE = {
"en": {
"from": "user@acme.com",
"subject": "Duplicate charge on invoice #4411",
"body": "Hi, we were billed twice for March. Please refund the duplicate today or we will cancel our plan.",
},
"ko": {"body": "3월 요금이 두 번 결제됐습니다. 중복 결제를 환불해 주시지 않으면 해지하겠습니다."},
}
QUESTIONS = {
"department": {
"type": "choice",
"instructions": "Which department should handle this request?",
"criteria": {
"billing": "invoices, payments, refunds",
"technical": "bugs, outages, system errors",
"sales": "pricing, new contracts",
"other": "everything else",
},
},
"urgency": {
"type": "score",
"instructions": "How urgent is this request?",
"criteria": ["not urgent", "soon", "critical deadline or blocking issue"],
},
"churn_risk": {"type": "noul", "instructions": "Does the user threaten to cancel or leave?"},
}
# --- Laya 측: 같은 상태를 영어/다국어 체크포인트로 각각 실행 ---
for subfolder, key in [(None, "en"), ("multilingual", "ko")]:
agent = laya.load("convaiinnovations/laya", subfolder=subfolder)
agent.predict(STATE[key], QUESTIONS) # warmup
rows = []
for _ in range(3):
t0 = time.time()
res = agent.predict(STATE[key], QUESTIONS)
dt = (time.time() - t0) * 1000
a = res["answers"]
rows.append((
a["department"]["choice"], round(a["department"]["confidence"], 2),
round(a["urgency"]["score"], 2), round(a["churn_risk"]["noul"], 3), round(dt),
))
print(f"[laya/{key}] 3 runs:", rows)
del agent
# --- TypeSafe Jev 측 (jev-1.13.0, 실제 호출 응답 기록) ---
# 요청: state = STATE(위와 동일), questions = 6개(department_en/urgency_en/churn_en/department_ko/urgency_ko/churn_ko,
# 각 instructions에 "Judge ONLY the state field `en`/`ko`" 명시)
JEV_RESPONSE = {
"model": "jev-1.13.0",
"elapsedMs": 791,
"department_en": {"choice": "billing", "confidence": 1, "probabilities": {"billing": 1, "technical": 0, "sales": 0, "other": 0}},
"urgency_en": {"score": 1.87, "confidence": 0.80},
"churn_en": {"noul": 0.97},
"department_ko": {"choice": "billing", "confidence": 1},
"urgency_ko": {"score": 1.85, "confidence": 0.77},
"churn_ko": {"noul": 0.97},
"usage": {"input_tokens": 788, "output_tokens": 159},
}
print("Jev response:", json.dumps(JEV_RESPONSE, ensure_ascii=False, indent=1))
# 기대 비교 결과 (상세는 COMPARISON.md):
# department: 둘 다 billing / urgency: Jev 1.87 vs Laya 1.44~1.79 / churn: Jev 0.97 vs Laya EN 0.825, KO 0.009(미스)
#!/data/data/com.termux/files/usr/bin/bash
# Laya를 Termux에서 돌리기 위한 설치 스크립트 (Galaxy S25 / SM8750, Python 3.14에서 검증)
set -e
# 1. torch + tokenizers — TUR 사전 빌드 (PyPI manylinux 휠은 안드로이드에서 안 돌아감)
pkg install -y python-torch python-tokenizers
# 2. safetensors — apt에 없어서 Rust 빌드
pkg install -y rust
pip install maturin
pip install safetensors
# 3. transformers + laya — 의존성 자동 설치를 피해 수동 구성
pip install --no-deps laya transformers "huggingface_hub<1.0"
pip install filelock regex requests tqdm packaging pyyaml
# 4. transformers 4.57.6의 tokenizers 버전 상한(<=0.23.0) 패치 — apt tokenizers는 0.23.2
SP=$(python3 -c "import site; print(site.getsitepackages()[0])")
sed -i 's/tokenizers>=0.22.0,<=0.23.0/tokenizers>=0.22.0/' \
"$SP/transformers/dependency_versions_table.py"
# 5. 확인
python3 - <<'PY'
import torch, transformers, tokenizers, safetensors, huggingface_hub, laya
print("torch", torch.__version__)
print("transformers", transformers.__version__)
print("tokenizers", tokenizers.__version__)
print("safetensors", safetensors.__version__)
print("huggingface_hub", huggingface_hub.__version__)
print("laya OK")
PY
echo "완료: python3 laya_test.py 로 실행"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment