Junie 에이전트를 위한 Qwen 3.6 모델 최적화 방법
얼마 전 저희는 사용자가 다양한 하드웨어 구성에서 로컬 추론으로 Junie를 완전히 로컬에서 실행할 수 있도록 하는 장기 프로젝트를 시작했습니다. 큰 기대를 등에 업고 최근에는 Qwen3.6-27B를 기반으로 MacBook M5에서 작동하는 Junie Local의 첫 버전을 출시했습니다.
이 블로그 글에서는 이를 구현하기 위해 어떤 작업이 필요했는지, 그리고 Qwen3.8-27B가 아닌 Qwen3.6-27B로 출시한 이유를 공유합니다. 이 작업에서 저희는 Junie 에이전트 자체부터 선택된 추론 엔진까지 전체 스택을 최적화했습니다.
먼저 Junie부터 살펴보겠습니다. 어차피 로컬 모델과의 상호작용이 시작되는 곳이 Junie이기 때문입니다.
Junie 최적화
에이전트의 롤링 컨텍스트 확장
다른 코딩 에이전트와 마찬가지로 Junie에도 모든 작업이 수행되는 메인 실행 루프가 있습니다.
- 먼저 사용자가 작업을 지정합니다.
- 이어서 Junie가 작업을 LLM에 전송합니다.
- 그러면 LLM이 Bash 명령어, 파일 읽기/쓰기 지시 등의 도구 호출로 응답합니다.
- 마지막으로 Junie가 이 명령의 결과를 다시 LLM에 전송합니다.
내부에서 일어나는 과정을 아주 단순하게 나타낸 차트는 다음과 같습니다.

차트에서 볼 수 있듯이 LLM은 컨텍스트를 계속 확장하는 요청을 받으며, 그 덕분에 이미 처리한 요청의 정보를 일부 재사용할 수 있습니다. 더 구체적으로는 이전 요청의 프리필 데이터를 다음 요청에 재사용할 수 있으며, 이 데이터를 KV 캐시라고 합니다.
그러나 동일한 세션에서 Junie에게 두 번째 작업을 요청하면 Junie는 컨텍스트에서 관련 부분만 가져와 창에 넣습니다.

클라우드 모델에서는 보통 이 방식이 완벽하게 작동합니다. 모델이 일부 파일의 내용을 다시 필요로 하더라도 액세스를 요청해 다시 처리할 수 있기 때문입니다. 게다가 프리필도 매우 빠릅니다.
하지만 로컬 모델에서는 상황이 다릅니다. 프리필이 그다지 빠르지 않고, 파일을 ‘읽는’ 데 실제로 상당한 시간이 걸립니다.
이 문제를 해결하기 위해 로컬 추론 로직을 변경했습니다. 이제 모든 새 요청을 롤링 컨텍스트에 직접 추가합니다.

이 방식이라면 이전 작업의 KV 캐시를 재사용할 수 있습니다. 즉, 모델이 이미 파일을 읽었다면 해당 파일이 컨텍스트 창에 그대로 남기 때문에 다시 ‘읽을’ 필요가 없습니다.
초기 재사용 가능 접두사 최대화
저희가 적용한 또 다른 유사한 최적화는 새 코딩 세션을 시작할 때 Junie가 보내는 시스템 프롬프트와 초기 컨텍스트에 관한 것입니다.
앞 섹션에서는 흐름을 다소 단순화해 설명했습니다. 실제 첫 번째 LLM 요청에서는 차트에 표시된 것보다 훨씬 많은 데이터가 LLM에 전송됩니다.

보시는 것처럼 완전히 다른 정보 집합이 LLM에 전달됩니다. 그리고 이 모든 정보는 새 세션을 시작할 때마다 전송되고 처리됩니다. 저희는 당연히 이 정보를 어떤 방식으로든 캐시 처리할 수 있기를 바랐습니다 🙂
이를 위해 이 데이터를 전송하는 순서를 변경했습니다.

또한 추론 엔진에 특별한 로직을 추가해 사용자의 요청까지 이어지는 접두사 전체를 캐시 처리했습니다. 덕분에 같은 프로젝트 내 이후 작업에서는 이를 그대로 재사용할 수 있습니다. 다만 사용자의 요청 뒤에 오는 프로젝트 컨텍스트는 제외했습니다. 이 컨텍스트는 상당히 작고 대부분 최상위 프로젝트 파일로 구성되어 있어 자주 바뀔 수 있기 때문입니다.
진행 상황을 작업에 업데이트
더 강력한 클라우드 모델을 사용할 때 Junie는 사용자에게 표시할 업데이트가 담긴 XML과 유사한 형식의 특수 블록을 추가하도록 LLM에 요청합니다. 아쉽게도 Qwen 3.6은 이러한 요청을 대부분 무시합니다. 한편 모델은 LLM 요청 결과의 일부로 자신의 작업을 일반 텍스트로 작성합니다. 즉, LLM이 도구 호출과 함께 이를 설명하는 텍스트를 전송합니다.
따라서 해결책은 간단합니다. Qwen 3.6이 생성한 이 텍스트를 사용자에게 보여 줄 업데이트로 사용하면 됩니다. 이런 조정은 모델마다 다릅니다. 즉, Qwen 3.6이 이렇게 동작한 것은 운이 좋았던 경우입니다. 어떤 모델은 이 부분에 아무것도 출력하지 않고, 다른 모델은 지나치게 많은 텍스트를 생성합니다.
불필요한 LLM 호출 제거
다음 최적화인 모든 선택적 LLM 요청 비활성화는 사소해 보일 수 있지만, 에이전트의 효율성을 높이는 데 실제로 큰 도움이 되었습니다. 구체적으로는 작업에 대한 짧은 설명을 생성하는 모든 로직을 비활성화했습니다. 이 과정에서 일부 UX를 희생한 것은 사실이지만, 그 손실이 지나치다고 판단하지는 않았습니다. 또한 M5에서 LLM 요청을 가장 효율적으로 처리하는 방법은 순차 처리이므로 멀티 에이전트 모드도 완전히 비활성화했습니다. 여러 에이전트를 활성화하더라도 어차피 추론 엔진이 병목이 되므로 의미가 없기 때문입니다.
모델 매개변수 최적화
reasonning_effort: None
Qwen3.6-27B의 클라우드 버전을 내부 테스트하면서 추론(reasoning)을 활성화해도 품질이 크게 향상되지 않는다는 점을 확인했습니다. 그래서 로컬 버전에서는 추론을 완전히 비활성화하기로 했습니다. 이는 상당히 중요한 부분입니다. 추론 엔진 관점에서 추론 토큰은 기본 응답을 생성할 때 사용하는 토큰과 동일하기 때문입니다. 결과적으로 추론을 비활성화하면 생성해야 할 토큰 수가 2~3배로 줄고, 품질에는 미미한 영향만 주면서 작업 실행 속도를 2배 높일 수 있습니다.
양자화
4비트 버전은 벤치마크에서 8비트 버전보다 성능이 아주 조금 낮을 뿐이며, 생성이 메모리 병목의 영향을 받기 때문에 4비트 버전을 사용하면 8비트 버전보다 약 2배 빠릅니다. 이에 따라 저희는 4비트 버전을 선택했습니다. 그런데 8비트 버전과 4비트 버전의 프리필 속도를 비교해 보니 두 버전의 속도가 같았습니다… 이상하다고 판단해 더 깊이 들여다봤습니다.
추론 엔진 최적화
프리필 최적화 기법
애초에 왜 프리필을 이렇게까지 신경 쓰는지 궁금할 수도 있습니다. 인터넷에는 생성 속도 벤치마크와 최적화 옵션이 넘쳐나기 때문입니다.
실제로 디스크리트 GPU(예: RTX 5090)라면 프리필의 중요성에 의문을 가질 만합니다. 이런 하드웨어에서는 프리필이 연산 병목의 영향을 받고 이산형 GPU가 일반적으로 매우 강력하기 때문에 프리필이 실제로 매우 빠릅니다. 기본 구성에서도 약 3,700t/s의 프리필 속도를 얻을 수 있습니다. 반면 기본 상태의 M5에서는 약 650t/s 수준이었습니다. 따라서 모델이 파일 내용을 요청해 조사하는 동안에는 대부분의 시간이 생성이 아닌 프리필에 소요되었습니다!
더 큰 문제는 프리필 속도에서 4비트, 8비트, 16비트 양자화 사이에 아무런 차이가 없었다는 점입니다. 왜 그랬을까요? 프리필은 연산 병목의 영향을 받기 때문에 메모리 속도의 제약을 전혀 받지 않습니다. 알고 보니 프리필 중 대부분의 행렬 연산이 전체 16비트 모드에서 수행되고 있었습니다. 즉, 모든 4비트 가중치를 연산 전에 16비트 숫자로 변환하고 있었습니다. 그런데 M5 프로세서에는 16비트 연산보다 훨씬 빠른 8비트 숫자용 특수 연산이 있습니다. 이에 프리필 중 일부* 행렬 연산을 8비트로 전환하는 패치를 MLX-VLM 패키지에 적용했고, 프리필 속도가 약 40% 향상되었습니다!
참고로 이것이 저희가 M5 칩에 집중하기로 한 가장 큰 이유입니다. M4 칩에는 이러한 8비트 산술 명령어가 없고, M4의 16비트 산술 연산에서는 프리필이 20~30% 더 느립니다.
*Qwen3.6-27B는 full-attention 레이어와 self-attention 레이어를 모두 사용합니다. 저희가 확인한 결과, 4비트 양자화에서도 full-attention 가중치는 전체 16비트 정밀도로 계속 저장됩니다. 이러한 레이어는 어떤 경우에도 전체 정밀도를 유지하므로 해당 최적화를 적용하지 않았습니다. 실제로 메모리/연산량을 줄일 수 있는 셀프 어텐션 레이어에만 적용했습니다. MLX-VLM의 패치 링크는 여기에서 확인할 수 있습니다. 더 나아가 모델의 구성 파일을 편집하기만 하면 같은 최적화를 vLLM에도 적용할 수 있습니다. 구성 예시
MTP 및 n-gram 일치를 통한 추측 디코딩
표준 최적화로 다음 방법을 적용했습니다.
- 별도의 드래프트 모델을 사용하는 MTP(Multi-Token Prediction): 더 작은 드래프트 모델이 여러 토큰을 미리 제안하면 본 모델이 이를 검증하는 추측 디코딩 방식입니다.
- N-gram 추측 디코딩: 드래프트 모델을 사용하는 대신 컨텍스트에서 이전에 반복된 토큰 시퀀스를 찾아 이에 일치하는 항목을 기반으로 다음 토큰을 ‘예측’하는 방식입니다.
두 가지 방식을 동시에 활성화했습니다. 실제 생성 과정에서는 MTP가 제안한 약 3개 토큰뿐 아니라 n-gram 방식에서 승인된 최대 8개의 추가 토큰까지 한 번에 승인하는 경우가 있습니다. 아래 그림은 생성된 원시 토큰을 각 승인 토큰을 생성한 방식(드래프트 모델과 n-gram)에 따라 색상으로 구분해 보여 줍니다.

두 방식을 함께 사용하면 생성 속도가 최대 2배 빨라집니다.
Qwen3.8-27b
그렇다면 이 모든 점을 고려할 때 왜 3.6 대신 3.8을 쓰지 않았을까요?
안타깝게도 Qwen 3.8이 제대로 작동하려면 추론 모드를 활성화해야 합니다. 이를 활성화하지 않으면 결과 품질이 크게 떨어집니다. 일반적인 작업에서는 심지어 완전히 실패해 동일한 도구 호출을 무한히 반복하는 루프에 빠지기도 합니다. 반면 추론 모드를 활성화하면 생성되는 토큰 수가 크게 늘어납니다. 중간 수준의 추론 작업에서는 약 5배 많은 토큰이 생성됩니다. 프리필 시간은 대체로 일정하게 유지되기 때문에, 실질적인 속도 저하는 5배가 아니라 약 4배에 가깝습니다. 그래도 상당한 비용입니다. 그래서 현재 Mac 하드웨어에서는 Qwen3.6-27b가 여전히 더 나은 선택입니다.
마무리
지금까지의 과정을 통해 일반적인 에이전트 프로그래밍 작업에서 생성 t/s에만 집중하는 것이 잘못된 접근 방식이라는 점을 확인하셨기를 바랍니다.
다음을 포함한 스택의 모든 부분을 최적화해야 합니다.
- 생성 및 프리필: 토큰 단위 디코딩 프로세스와 초기 컨텍스트 처리 단계.
- 모델 매개변수 및 양자화: 모델의 가중치 정밀도와 구성.
- 에이전트 하네스: 모델을 구동하는 주변 오케스트레이션 계층(도구 호출, 제어 흐름, 프롬프트 로직).
앞으로 저희는 바로 이 작업을 계속해 나갈 계획입니다. M5 지원은 이제 시작에 불과합니다. 이미 DGX Spark와 RTX 5090용 프로토타입을 보유하고 있으며, 24GB 카드도 검토하고 있으니 앞으로의 소식도 기대해 주세요!
게시물 원문 작성자