64GB의 Mac Studio M4 Max에서, MTP가 적용된 Qwen3.8-27B는 제가 이미 구동하고 있는 35B 혼합 전문가(mixture-of-experts) 모델을 대체하지 못합니다. oMLX 0.7.0으로 측정된 결과에 따르면, Qwen은 90 토큰/초 대비 55 토큰/초를 생성했으며, 800 토큰 프롬프트에 대해 첫 토큰을 생성하는 데 걸린 시간은 0.49초 대비 3.46초였습니다. 코드 검토 테스트에서는 두 모델 모두 추론(reasoning) 기능을 끈 상태에서 동일한 버그를 포착했으며, Qwen이 이 기능을 켠 상태에서 더 적게 포착했습니다.
결정적인 차이는 첫 토큰 생성 시간
Qwen3.8-27B는 Ornith 1.5보다 답변을 시작하는 데 7배나 더 오랜 시간이 걸립니다: 평균적으로 8개 요청에 대해 0.49초 대비 3.46초입니다. 출력 속도는 더 가깝습니다, 각각 55.1 토큰/초와 90.4 토큰/초입니다. 채팅에서는 이 격차가 성가십니다. 코딩 에이전트나 코드 검토는 긴 프롬프트를 여러 번 연속으로 보내는데, 이때 첫 토큰을 기다리는 시간이 가장 큰 비용을 차지합니다.
oMLX는 Apple Silicon Mac용 추론 서버로, OpenAI와 호환되는 API 뒤에서 MLX 모델을 서비스합니다. 저는 두 모델 모두 이를 사용하여 실행했으며, 각각 더 빠르게 생성하기 위한 고유한 기술이 있습니다.
Qwen3.8-27B는 밀집(dense) 모델입니다: 270억 개의 모든 파라미터가 매 토큰마다 작동합니다. 이 모델은 multi-token prediction이라는 MTP와 함께 제공되는데, 이는 모델이 다음 몇 개의 토큰을 초안 작성하여 한 번의 패스로 검증할 수 있게 하는 추가 헤드입니다. 반면, Ornith 1.5 35B-A3B는 혼합 전문가(mixture of experts) 모델이며, 350억 개의 파라미터 중 토큰당 약 30억 개가 활성화됩니다. 저는 이를 DFlash와 함께 실행하는데, 여기서 별도의 작은 초안 모델이 토큰 블록을 제안하고 주 모델이 이를 확인합니다.
차트는 두 가지 측정값을 나란히 보여줍니다. 제가 신경 쓰는 첫 번째 막대 쌍입니다.
Bar chart of two measurements on oMLX 0.7.0. Time to first token on an 800-token prompt: Ornith with DFlash 0.49 seconds, Qwen3.8-27B with MTP 3.46 seconds. Output speed: Ornith 90.4 tokens per second, Qwen 55.1. Ornith wins both.
저는 aiperf 0.12.0을 사용하여, 모델을 한 번에 하나씩 로드하고, 실행 전에 서버를 재시작하며, 다른 트래픽이 없는 상태에서 측정했습니다. 이 명령어가 사용됩니다. 이는 모든 OpenAI 호환 서버와 작동합니다:
aiperf profile --url http://localhost:8010 --model <model-id> \
--endpoint-type chat --streaming \
--concurrency 1 --request-count 8 --num-warmup-requests 2 \
...
oMLX 업그레이드가 Qwen에 약 20% 도움을 주었지만, 충분하지는 않았습니다
oMLX 0.6.4에서 0.7.0으로 이동하면서 Qwen3.8-27B의 코딩 프롬프트당 속도는 초당 5759 토큰에서 71.7로, 산문(prose)은 초당 4648 토큰에서 5556토큰으로 향상되었습니다. 각 셀마다 두 번 실행했으며 출력 토큰은 400개였습니다. oMLX 0.7.0 릴리스 노트에는 이 모델에 대한 더 빠른 MTP 검증 및 디코딩이 나열되어 있으며, 수치도 이를 뒷받침합니다. Ornith는 변화가 없었습니다: 코드는 초당 103121토큰, 산문은 99~102토큰이었습니다.
또한 명백한 조절 장치(knob)도 시도해 보았습니다. 이 모델의 MTP 기본값은 사이클당 최대 4개의 토큰까지 초안을 작성하며, oMLX는 이 상한선을 높일 수 있게 해줍니다. 6으로 설정했을 때 출력 속도는 초당 53.9토큰이었고, 8로 했을 때는 52.4토큰이었으며, 기본값인 55.1토큰과 비교되었습니다. 적응형 컨트롤러(adaptive controller)가 이미 이득이 되는 깊이를 선택하므로, 저는 설정을 원래대로 되돌렸습니다.
코드 검토 테스트에서 두 모델은 동률입니다
추론 기능(reasoning)을 끈 상태에서, Qwen3.8-27B는 심어진 버그 20개 중 9개를 잡아냈고 Ornith 1.5는 8개를 잡아냈습니다. 20개의 케이스에서 한 개의 버그 차이는 노이즈에 불과합니다. 이 테스트는 제 저장소 네 개에서 가져온 실제 커밋 20개를 사용하며, 추가된 라인 중 하나의 연산자(<를 <=, ==를 !=, 누락된 not)만 변경한 후 모델에게 도구 없이 단일 호출로 diff를 검토하도록 요청합니다. 각 케이스는 깨끗한(clean) diff와 쌍을 이루며, 모델이 변형된 라인(mutant)에서 플래그를 지정하고 깨끗한 버전에서는 그렇지 않을 때에만 점수를 얻습니다.
모델별 비교 지표
| Model | Reasoning | Bugs caught | Unreadable answers | Findings per clean diff | Median time |
|---|
| Qwen3.8-27B + MTP | off | 9 of 20 (45%) | 4 of 40 | 0.7 | 10 s |
| ... | | | | | |
Ornith 모델의 측정은 2026년 10월 2일에 oMLX 0.6.4에서, Qwen 모델의 측정은 10월 12일 0.7.0에서 수행되었습니다. Reasoning을 off 상태로 했을 때 차이점은 오탐지(false alarms)입니다. Qwen은 실제로는 문제가 없는 코드에서도 절반만큼 많은 문제를 보고하며, 검토당 시간이 세 배 더 오래 걸립니다.
Reasoning을 on으로 설정해도 Qwen에게는 도움이 되지 않습니다. Reasoning을 끄고 측정했을 때 9개였던 버그를 20개 중 6개만 잡아냈으며, 40개의 답변 중 절반은 전체 12,000 토큰 출력 제한을 소모하여 그 결과 12개는 유효한 JSON에 도달하지 못했습니다. Ornith 역시 같은 방식으로 과도하게 생각(overthinks)하며 40개 답변 중 13개를 놓치지만, 20개 중 8개를 유지하고 검토당 시간이 절반 정도 걸립니다. 세 분이 걸리고 3분의 1 확률로 파싱에 실패하는 검토는 어느 모델에게도 쓸모가 없습니다.
두 모델은 메모리상에서 함께 적재되지 않습니다
Qwen3.8-27B는 로드 시 16.1 GB를 사용하고 Ornith 1.5는 19.8 GB를 사용합니다. 64 GB 장비에서는 괜찮아 보이지만, oMLX 0.6.4 환경에서 두 모델 모두 적재되었고 스왑 메모리는 16 GB 중 15.3 GB까지 증가했습니다. 하지만 0.7.0 버전의 메모리 가드(memory guard)는 이를 거부합니다. Qwen이 바쁜 동안 Ornith에 대한 모든 요청은 다음과 같은 메시지를 받았습니다:
POST /v1/responses → 507: Cannot load Ornith-1.5-35B-A3B-MLX-4bit: projected memory
38.24GB would exceed the dynamic memory ceiling 34.51GB
Qwen이 유휴 상태일 때, oMLX는 한 모델을 제거(evicts)하고 다른 모델을 로드하며, 로그에는 언로드(unload)가 즉시 중단된 것(immediate abort)으로 표시되므로, 제거된 모델에서 진행 중이던 요청은 손실됩니다. 저는 우연히 부하 상태에서의 상황을 알게 되었습니다. 또 다른 도구가 Ornith에 계속 요청을 보내는 동안 Qwen의 Reasoning을 on으로 설정하고 90분 동안 검토 테스트를 실행했습니다. oMLX가 Ornith를 로드했고, 6초 후 Qwen의 프롬프트를 위해 제거(evict)한 다음 다시 로드했으며, 결국 답변 중간에 Qwen을 제거했습니다. Qwen의 40개 요청 중 19개가 실패했습니다.
Qwen을 Ornith와 두 번째 의견으로 함께 사용하길 바랐지만, 그건 불가능합니다. 둘 중 하나를 골라야 하고, 이 수치들을 보니 저는 Ornith를 선택하겠습니다.
Qwen은 때때로 자체 JSON을 잘라내고, 수정하는 데 속도의 절반이 소요됩니다
40개의 검토 답변 중 5개에서 Qwen3.8-27B는 {"findings":[]만 반환하고 종료되어, 닫는 중괄호(})가 누락되었습니다. 이는 오직 다섯 개 또는 여섯 개의 토큰으로 이루어진 빈 답변에서만 발생했습니다. 영향을 받은 5개의 차이(diff)를 네 가지 설정 하에 각각 세 번씩 다시 실행했는데, MTP(Minimum Token Penalty)를 켜고 끌 때와 존재 페널티(presence penalty)를 1.5와 0으로 했을 때 모두 그러했습니다. 잘린 답변은 MTP가 꺼져 있고 페널티도 없을 때 최대 15회 중 4회까지 나타났습니다. 제가 알기로는 이 모델이 가끔 그냥 일찍 종료되는 것 같습니다.
제약 디코딩(Constrained decoding)이 이를 해결합니다. oMLX에는 xgrammar를 통해 내장되어 있지만, Homebrew 포뮬러는 요청하지 않으면 이 기능을 제외합니다:
brew reinstall jundot/omlx/omlx --with-grammar
요청에 JSON 스키마가 포함된 경우, 30개 답변 중 0개가 파싱할 수 없었습니다. 다이어그램은 이것이 비용으로 얼마인지 보여줍니다. 문법(grammar)과 MTP는 동시에 실행될 수 없으므로, 해당 요청들은 약 초당 25 토큰의 일반 디코딩으로 폴백됩니다.
[Flow diagram of a review request to Qwen3.8-27B. Without a JSON schema it uses MTP decoding at about 55 tokens per second and up to 4 of 15 answers are unparseable. With a JSON schema it uses grammar-constrained decoding with MTP switched off at about 25 tokens per second and 0 of 30 answers are unparseable.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjh06kdj4g8hlxrgugws9.png
아직 해결되지 않은 문제들
Qwen의 추론(reasoning)에 대한 행은 두 번의 실행을 혼합합니다. 21개의 답변은 위에서 설명한 아침 실행에서 나온 것이고, 서버의 다른 트래픽이 있었으며, 저는 모든 것을 일시 중지하고 오후에 실패했던 19개 요청을 반복했습니다. 저는 아침 답변들을 세었는데 그것들은 완전했기 때문이지만, 40개를 모두 깨끗하게 한 번에 다시 실행하지는 않았습니다. Qwen의 비전(vision)도 테스트하지 못했습니다. Ornith의 양자화된 빌드는 비전 기능이 없으므로, 만약 Qwen의 것이 MTP와 함께 작동한다면 제가 이미지용으로 보관하는 세 번째 작은 모델을 폐기할 수 있을 것입니다. 또한 DFlash를 사용하는 Ornith가 JSON 스키마에 얼마의 비용이 드는지도 알지 못합니다.
속도 테스트는 8개 이상의 요청과 수천 개의 토큰으로 구성된 프롬프트를 필요로 합니다. 저는 첫 토큰 간격(first-token gap)이 길이와 함께 벌어질 것으로 예상하지만, 한 가지 크기만 측정했습니다.
현재로서는 Ornith가 유지됩니다. 만약 Mac용 로컬 모델을 선택한다면, 초당 토큰 수(tokens per second)를 보기 전에 실제로 전송하는 프롬프트 길이에서의 첫 토큰 시간을 측정하십시오. 제가 만든 다른 도구들은 프로젝트 페이지에 있으며, 제가 매크로 패드 소프트웨어를 만들기 위해 4개의 AI 어시스턴트를 활용한 방법에서는 동일한 설정의 에이전트 측면을 다루고 있습니다.