회사 테크

6배 작은 모델의 대가 — Qwen3.8-27B 4bit, Terminal-Bench 89개 1회차 35 대 45

Qwen3.8-27B MLX 4bit(17GB)를 Flash Next(100GB)와 같은 조건으로 Terminal-Bench 2.1 89개에 돌렸습니다. 1회차 35/89(39.3%) vs 원본 45·Uncensored 41. 갈린 태스크, 반복 루프 15건, 속도, 운영 메모

삽질하는개발자

뉴런 네트워크 일러스트

요약

9월 1일 글에서 Qwen3.8-27B 4bit을 “메모리가 빠듯하면 이걸 쓰라, 품질은 동등”이라고 썼습니다. 단문 프롬프트 몇 개로 본 결론이었습니다. Terminal-Bench 시리즈를 닫고 나니 같은 하네스에 이 모델을 넣어보지 않을 이유가 없었습니다.

ddalcu/Qwen3.8-27B-MLX-Serve-4bit (17GB, affine 4bit g64, MTP 헤드 내장)를 Flash Next 두 모델과 동일 조건(terminus-2, ctx 120K, temperature 0.3, reasoning xhigh, 타임아웃 1.0×)으로 89개 전부 한 번 돌렸습니다. 31시간 11분이 걸렸습니다.

결과: 35 / 89 (39.3%) — PASS 31, 타임아웃 시점에 파일이 맞아 통과한 PASS(TO) 4, FAIL 16, TO 38, ERR 0입니다.

1회차 (pass@1) 통과 비율 소요
Flash Next 원본 mixed-4-8bit (100GB) 45 / 89 50.6% 24.7h
Flash Next Uncensored 4bit (ARC4NUM) 41 / 89 46.1%
27B 4bit (17GB) 35 / 89 39.3% 31.2h

참고로 Flash Next 두 모델의 pass@5는 64와 59입니다. 27B는 한 번만 돌렸으니 그 표와 직접 비교할 수는 없고, 이 글은 1회차 대 1회차만 봅니다. 다섯 번 돌릴지는 아래에서 말합니다.

한 줄로 줄이면 디스크 6분의 1, 정답 10개 손해, 그리고 속도 이득은 없었습니다. 손해 10개의 내용이 이 글의 본론입니다.

조건 한 가지 정정

이전 글들은 reasoning_effort=high로 썼고 이번엔 xhigh입니다. 다른 조건처럼 보이지만 실측해보니 mlx-serve 26.8.11은 Qwen3.8 템플릿이 모르는 highxhigh로 처리합니다(프롬프트 토큰 수가 53으로 동일). 즉 시리즈 내내 실제 조건은 xhigh였고, 이번 실행은 그것을 명시했을 뿐입니다. 동일 조건이 맞습니다.

두 모델의 구조도 적어둡니다. 둘 다 선형 어텐션 3 : 풀 어텐션 1의 하이브리드입니다. 27B는 64층 dense(풀 어텐션 16층), Flash Next는 48층 MoE(512 전문가 중 10개 활성, 풀 어텐션 12층 + 어텐션 인덱서)입니다. 활성 파라미터로는 27B가 Flash Next(~6B)의 4배쯤 되지만, 총 파라미터와 디스크는 6분의 1입니다.

어디서 갈렸나

세 모델 1회차를 겹쳐보면 이렇습니다.

구분 개수 태스크
셋 다 통과 24 짧은 것들 — fix-git, git-leak-recovery, openssl-selfsigned-cert, kv-store-grpc 등
Flash Next 둘 다 통과, 27B만 실패 10 bn-fit-modify, db-wal-recovery, distribution-search, fix-ocaml-gc, mailman, merge-diff-arc-agi-task, mteb-retrieve, polyglot-c-py, sqlite-with-gcov, torch-tensor-parallelism
Flash Next 둘 다 실패, 27B만 통과 6 build-cython-ext, code-from-image, count-dataset-tokens, extract-elf, password-recovery, tune-mjcf
Flash Next 하나만 통과, 27B 통과 5 filter-js-from-html, overfull-hbox, pytorch-model-recovery, reshard-c4-data, financial-document-processor
Flash Next 하나만 통과, 27B 실패 13 caffe-cifar-10, crack-7z-hash, mcmc-sampling-stan, qemu-startup 등
세 모델 다 실패 31 pass@5로도 못 푼 21개 + regex-log, torch-pipeline-parallelism, raman-fitting 등

격차 10 = (원본 45 − 27B 35)인데, 태스크 단위로 보면 27B가 원본에 없는 9개를 풀고 원본이 27B에 없는 19개를 풀었습니다. 1회차끼리의 비교는 이 정도로 흔들립니다. 시리즈에서 확인했듯 pass@1과 pass@5 사이 편차가 19개였습니다.

그래서 pass@5 결과와도 겹쳐봤습니다. 27B가 푼 35개 중 34개는 원본 pass@5 안에 있고, 유일한 예외가 filter-js-from-html(원본 5회 전패, Uncensored는 통과)입니다. Uncensored pass@5 기준으로는 extract-elf와 tune-mjcf 2개가 Uncensored 5회 전패였습니다. 두 Flash Next의 pass@5 합집합(68개) 바깥의 21개는 27B도 0개입니다. 27B가 새로 연 문은 없습니다. 다만 6분의 1 크기가 “큰 모델이 다섯 번 시도해야 풀리는 것”을 첫 시도에 몇 개 잡았다는 것은 적어둘 만합니다.

27B만 통과한 6개가 어떻게 풀렸는지 봅니다.

  • tune-mjcf (14.2분, 30턴) — MuJoCo 케이블 시뮬레이션을 60% 시간 안에 같은 결과로 돌려야 하는 15분 태스크입니다. Flash Next 원본은 1회차 TO 뒤 2회차에 통과했고 Uncensored는 5회 전패였습니다. 27B는 8턴째 프로파일링으로 “솔버가 병목”임을 찾고, Newton → PGS를 돌려본 뒤 solver="PGS" 한 줄로 2.18배(46%)를 만들었습니다. 한 번 만에 풀었는데 15분 제한에 48초 남았습니다.
  • extract-elf (PASS(TO), 15.0분, 35턴) — ELF에서 메모리 값을 뽑는 태스크입니다. 예시가 0x400000인데 바이너리의 .data는 0x4000이라 헤더를 30턴 동안 뒤지다가, 마지막 3턴에 “주소 체계를 여러 개 섞어 전부 출력”하는 쪽으로 돌아섰습니다. 타임아웃으로 끊겼지만 그 시점 파일이 채점을 통과했습니다. 정답을 이해한 것이 아니라 커버리지로 이겼습니다.
  • code-from-image (11.5분) — PNG 속 의사코드를 읽어야 하는데 PIL도 tesseract도 없습니다. pip으로 easyocr을 깔고 줄 단위로 잘라 OCR, SALT = b'0000TBENCH-SALT'를 복원해 sha256 체인을 계산했습니다. 힌트(“bee26a로 시작”)로 자기 답을 검증하고 끝냈습니다.
  • count-dataset-tokens (10.4분) — README가 401이라 다운로드된 캐시에서 찾아 읽고, “science 도메인 = biology·chemistry·physics 26개”로 좁혀 79,586을 냈습니다.
  • password-recovery (5.9분) — .dat 파일 안에서 ZIP 로컬 헤더(PK\x03\x04)를 찾아 app/launchcode.txt를 꺼냈습니다.
  • build-cython-ext (PASS(TO)) — pyknotid를 NumPy 2.x에 맞게 고치는 태스크입니다. 24턴째 “18개 테스트 통과”였는데 이후 git diff가 pager에 걸려 4턴을 날리고 타임아웃됐습니다. 파일은 이미 맞았습니다.

여섯 개의 공통점은 없습니다. 굳이 찾자면 “탐색 경로가 한 번에 맞았다”는 것이고, 그것은 다섯 번 돌리면 큰 모델도 다 잡은 태스크들입니다.

38개 타임아웃 — 이번 주인공은 반복 루프

TO 38개를 제한 시간별로 보면 15분 13개 · 30분 13개 · 60분 8개 · 40분 2개 · 20분 1개 · 200분 1개입니다. 15분·30분짜리 26개는 Flash Next에서도 봤던 “환경 설치하다 끝남”이 많습니다(mcmc-sampling-stan은 30분 내내 RStan 컴파일, adaptive-rejection-sampler는 R 설치 후 11턴). 그것은 시리즈에서 충분히 썼습니다. 이번에 새로 본 것은 똑같은 턴이 수십 번 반복되는 루프입니다.

궤적에서 “직전 턴과 메시지가 완전히 같은” 연속 구간을 세어봤습니다. TO 38개 중 15개에서 10턴 이상 동일 반복이 나왔습니다.

태스크 반복 전체 턴 무슨 턴이었나
regex-chess 156 172 “터미널이 멈춘 것 같다. Ctrl+C를 보내자” — 14턴째 ctx 21K에서 시작해 60분 내내 Ctrl+C
build-pov-ray 143 168 “SourceForge가 ’no’를 돌려준다. 다른 미러를 시도하자” — 같은 wget 143번, 200분 제한을 다 씀
custom-memory-heap-crash 64 141 아래 ②
extract-moves-from-video 57 136 grep -A 30 'FRAME 16' 같은 프레임 확인 명령을 57번
protein-assembly 41 62 OAS 데이터베이스 접속 시도 41번
break-filter-js-from-html 34 40 6턴째부터 끝까지 같은 BeautifulSoup 테스트 스크립트
polyglot-rust-c 30 36 // #if 0은 안 된다, 다른 전략을 쓰자” 하면서 같은 파일을 30번 씀
winning-avg-corewars · install-windows-3.11 · caffe-cifar-10 · path-tracing 24~31
distribution-search · polyglot-c-py · path-tracing-reverse 10~17 “계속 제자리를 돌고 있다(I’ve been going in circles)“고 쓰면서 같은 명령

regex-chess가 전형입니다. 14턴째 어떤 명령이 길게 걸리자 “터미널이 멈췄다”고 판단해 Ctrl+C를 보냈고, 그 관찰 결과(빈 프롬프트)를 보고 또 “멈췄다”고 판단했습니다. 관찰이 같으면 temperature 0.3에서 응답도 같고, 응답이 같으면 관찰도 같습니다. 324토큰짜리 턴이 156번, 60분입니다. build-pov-ray는 더 비쌉니다. 200분 제한 태스크라 3시간 20분이 wget 한 줄에 들어갔습니다.

Flash Next 시리즈에서도 반복 루프는 있었습니다. 하지만 467회 실행에서 눈에 띈 것은 대부분 130K 컨텍스트 벽에 부딪힌 뒤의 fallback 루프였지, ctx 20K에서 시작하는 순수 반복은 아니었습니다. 27B는 관찰이 조금이라도 달라질 여지가 없으면 빠져나오지 못합니다. polyglot-rust-c의 반복 메시지가 흥미롭습니다. “이 접근은 실패한다, 다른 전략이 필요하다”고 분석해놓고 같은 파일을 씁니다. 분석과 행동이 분리돼 있습니다.

같은 하네스에서 큰 모델이 덜 걸렸으니 모델 쪽 문제가 맞습니다. 하지만 하네스가 “직전 N턴과 명령이 같으면 개입”하는 것만으로 이 15개 중 대부분은 최소한 다른 시도라도 해봤을 것입니다. 시리즈 결론의 “반복 루프 감지 없음”이 작은 모델에서는 치명적이 됩니다.

② 130K 벽, 두 번. custom-memory-heap-crash(30분, 141턴)와 video-processing(60분, 747턴)입니다. 둘 다 컨텍스트가 130K에 닿은 뒤 서버 400(prompt 131,790 tokens exceeds ctx_size 131,072)을 받고, 하네스가 “Technical difficulties. Please continue”를 끼워 넣어 재시도하는 것을 남은 시간 내내 반복했습니다. video-processing 하나가 400 overflow 2,082건, 전체(2,223건)의 94%입니다. 시리즈에서 다섯 번째 본 패턴이라 새로울 것은 없고, 27B라고 다르지도 않았습니다.

16개 FAIL — “다 됐다”고 말하고 틀렸다

FAIL 16개 중 14개가 15분 안에 끝났습니다. 시간이 남는데 답이 틀린 유형이고, 마지막 턴은 거의 전부 “모든 요구사항이 검증됐다”로 끝납니다. 채점기가 무엇이라고 했는지 몇 개만 봅니다.

  • mteb-retrieve (3.1분, 8턴) — 코사인 유사도 5위 문서를 내라는 태스크입니다. HumanEval: Benchmarking Python code generation…을 냈고 정답은 MTEB: Massive Text Embedding Benchmark입니다. 8턴 만에 자신 있게 제출했는데 순위가 어긋났습니다.
  • sqlite-with-gcov (5.6분) — 마지막 턴에 “sqlite3가 PATH에 있다”고 썼습니다. 채점기는 sqlite3 not found in PATH. 자기 셸에서만 잡힌 PATH였습니다.
  • nginx-request-logging (3.5분) — Status code missing in logs. log_format에 $status를 넣었다고 26턴째 보고했는데, 실제 로그 줄에는 없었습니다.
  • db-wal-recovery (9.7분) — 11개 레코드를 복구했다고 했지만 Apple should have updated value 150 from WAL. WAL의 갱신을 적용하지 않고 원본 값(100)을 냈습니다.
  • torch-tensor-parallelism (14.5분) — world_size 1·2·4 테스트를 직접 돌려 통과시켰는데, 채점기는 bias 분할과 출력 allclose가 어긋난다고 했습니다.
  • mteb-leaderboard (30.8분, 63턴) — 원본은 1회차에, Uncensored는 5회차에야 푼 태스크입니다. codefuse-ai/F2LLM-v2-14B(0.7110)를 냈고 정답은 GritLM/GritLM-7B입니다. Uncensored가 네 번 그랬듯 “28개 태스크 전부 있는 모델”로 좁히는 조건을 못 잡았습니다.
  • configure-git-webserver (1.8분) — 10턴, “end-to-end 테스트 통과”. 채점기는 404입니다. 테스트 데이터를 지우면서 사이트도 지웠습니다.
  • merge-diff-arc-agi-task, mailman, bn-fit-modify, dna-insert, sparql-university, raman-fitting, train-fasttext — 같은 패턴입니다. bn-fit-modify는 학습한 DAG 자체가 틀렸고, mailman은 76턴을 쓰고도 announce 메시지 카운트가 안 맞았습니다.

Flash Next에서도 이 유형은 있었습니다(sparql-university의 복수 국적, extract-elf의 0.00%). 차이는 빈도입니다. 27B 1회차 FAIL 16개 중 10개가 원본 1회차에서는 PASS였습니다. “검증했다”는 문장이 실제 검증과 같지 않은 비율이 작은 모델에서 올라갑니다.

하나 더, regex-log (15분 TO, 5턴)입니다. 턴 4와 5가 각각 5,300토큰짜리 응답이었습니다. 테스트 케이스를 잔뜩 넣은 파이썬 스크립트를 “직전 시도가 너무 길었다”고 하면서 또 길게 썼습니다. 5턴에 15분입니다. 출력 길이 제어도 작은 모델에서 먼저 흔들립니다.

속도 — 17GB가 100GB보다 빠르지 않았다

이것을 기대하고 돌린 사람이 많을 것입니다. 서버 요청 로그(mlx-serve 26.8.11, MTP, 8-bit KV, M5 Max 128GB) 프롬프트 길이별 중앙값, tok/s입니다.

프롬프트 원본 프리필 / 디코드 Uncensored 27B 4bit
< 8K 162 / 73.3 234 / 57.8 206 / 53.9
8–32K 350 / 65.5 391 / 61.9 508 / 56.4
32–64K 378 / 50.8 324 / 53.0 407 / 41.1
64K+ 173 / 53.3 248 / 44.0 301 / 33.3
100K+ 256 / 27.0

짧은 프롬프트에서 27B 디코드는 원본의 74%, 64K 넘어가면 63%입니다. 프리필은 오히려 27B가 빠른 구간이 있습니다(8–64K). 디코드가 느린 이유는 단순합니다. 토큰당 활성 파라미터가 27B 대 ~6B입니다. MoE의 “실제로 도는 것은 6B”가 4bit dense 27B를 이깁니다. 긴 컨텍스트에서 격차가 벌어지는 것은 풀 어텐션 층이 16 대 12인 데다 Flash Next에는 어텐션 인덱서가 있어서로 보이는데, 이것은 구조 차이를 안 뜯어본 추정입니다.

에이전트 루프에서는 어떻게 되는지 봅니다. 27B 턴당 소요 중앙값 0.36분(원본 0.38, Uncensored 0.39), 턴당 출력 301토큰(345, 334)입니다. 같습니다. 27B와 Uncensored가 둘 다 통과한 27개의 소요 시간은 중앙값 3.0 대 2.8분, 27B가 빨랐던 것이 12개입니다. 시리즈 결론 그대로입니다. 에이전트 시간은 컨테이너 안 명령 실행과 프리필이 먹고, 디코드 차이는 묻힙니다. 작은 모델이 사서 얻는 것은 메모리뿐이고, 이 머신에선 그것이 필요 없었습니다.

운영 메모

  • 원래 09-08 21:07에 시작했다가 23:49에 처음부터 다시 돌렸습니다. 첫 5개 태스크 중 디코드가 43 → 20 tok/s로 반토막 났는데 모델이 아니라 Finder 폭주(CPU 148%, 7.6GB, fileproviderd 87%)였습니다. killall Finder로 즉시 정상이 됐습니다. 조건을 깨끗이 하려고 재시작했고, 이후 31시간 동안 Finder는 얌전했습니다.
  • 서버: 요청 4,870건, wedge 재시작 0회, 메모리 admission 거부 0건, RSS 17GB 고정입니다. Uncensored 5회차 글에서 권한 대로 --prefix-cache-mem 4GB로 시작했더니 100K 컨텍스트가 상주해도 hot-cache가 상한을 지켰고(퇴거 2,601회) 스왑은 늘지 않았습니다. 지난번 스왑 47GB 사고는 8GB 설정이 원인이 맞았습니다.
  • 400 overflow 2,223건, 위에 쓴 두 태스크가 전부입니다. 서버 부하는 없었습니다.
  • 31.2시간은 원본 24.7시간보다 6.5시간 깁니다. 모델이 느려서가 아니라 TO가 38 대 29라서입니다. build-pov-ray 200분 하나가 그 차이의 절반입니다.

다섯 번 돌릴 것인가

pass@5 표에 27B를 올리려면 네 번 더 돌려야 합니다. 1회차 실패 54개(FAIL 16 + TO 38)만 재시도해도 회당 20시간 안팎, 나흘입니다. 시리즈에서 배운 대로 pass@1 → pass@5 증분은 +18~19였으니 27B도 50 언저리까지는 갈 것입니다. 하지만 위의 15개 반복 루프는 temperature 0.3에서 재시도해도 같은 관찰 → 같은 응답으로 다시 빠질 가능성이 높고, 그러면 증분은 그보다 작습니다.

지금 결정은 보류입니다. 돌린다면 그 전에 하네스에 반복 감지를 넣는 것이 먼저입니다. 같은 명령을 같은 관찰로 세 번 받으면 개입합니다. 그것이 없는 채로 네 번을 더 돌리는 것은 build-pov-ray에 13시간을 또 주는 일입니다.

한 줄 요약: 27B 4bit은 1회차 35 대 45입니다. 새로 연 태스크는 없고, 15분 안에 “검증했다”고 틀리는 FAIL과 ctx 20K에서 시작하는 순수 반복 루프가 격차를 만들었으며, 디스크 6분의 1의 대가로 얻은 속도 이득은 이 머신에선 0입니다.