메뉴
HN
Hacker News 46일 전

macOS에서 로컬 코딩 에이전트 구축하기

IMP
7/10
핵심 요약

인터넷 연결 문제에 대비하고자 macOS 환경에서 완전히 로컬로 구동되는 코딩 에이전트 환경을 구축한 후기를 공유합니다. Apple Silicon(M1 Max) 환경에서 llama.cpp와 Gemma 4 모델, 그리고 다중 토큰 예측(MTP) 기술을 활용해 실사용 가능한 수준의 높은 추론 속도를 달성했습니다. 특히 Apple 전용 프레임워크인 MLX보다 llama.cpp와 MTP 조합이 더 빠른 속도를 보여준다는 흥미로운 벤치마크 결과를 포함하고 있습니다.

번역된 본문

최근 인터넷이 몇 번 끊기면서 코딩 에이전트를 사용할 수 없는 난감한 상황을 겪었습니다. 그래서 Gemma 4의 다중 토큰 예측(MTP, Multi-Token Prediction) 업데이트로 인해 속도가 2배 빨라졌다는 소식을 접하고, 직접 로컬 환경에서 구동해보기로 결심했습니다.

제가 원했던 로컬 코딩 에이전트 환경의 조건은 다음과 같았습니다. 첫째, Mac에서 실제로 사용할 수 있을 만큼 빨라야 합니다. 둘째, OpenAI 호환 API를 통해 작동해야 합니다(이를 통해 다른 개발 도구에서도 사용 가능). 셋째, 가급적 스크린샷이나 이미지를 처리할 수 있어야 하므로, 에이전트가 자신이 만든 결과물의 스크린샷을 입력받아 피드백할 수 있어야 합니다.

그리고 결국 성공했습니다! 첨부된 영상은 실시간 속도를 보여주며, 에이전트가 완벽하게 실용적인 수준의 속도로 응답하는 것을 확인할 수 있습니다. 약간의 테스트를 거친 후, 제가 최종적으로 구축한 환경은 다음과 같습니다:

  • macOS에서 Metal 가속을 활용하도록 빌드된 llama.cpp
  • GGUF 형식의 Gemma 4 26B-A4B 모델
  • 추측 디코딩(Speculative decoding)을 위한 Q8 MTP 드래프트 모델
  • Gemma 4 멀티모달 프로젝터(Multimodal projector)
  • 터미널 코딩 에이전트인 Pi

이 설정은 macOS 15.7.7이 실행 중인 64GB 통합 메모리를 갖춘 Apple M1 Max에서 테스트되었습니다.

모델

사용된 메인 모델은 gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf입니다. (Huggingface 링크: models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf) 이 파일의 크기는 약 16GB입니다. MTP 드래프트 헤드와 멀티모달 프로젝터를 포함하면 모델 폴더의 크기는 약 17GB입니다.

벤치마크에 사용된 프롬프트는 다음과 같았습니다: "통합 diff(unified diff)를 파싱하여 변경된 파일 경로를 반환하는 간결한 Python 함수를 작성하세요. 그리고 두 가지 예외 상황(Edge cases)을 설명하세요." 각 벤치마크 테스트는 약 128개의 토큰을 생성했습니다.

기준선(Baseline): llama.cpp + Metal

우선 Metal 가속이 적용된 llama.cpp를 통해 메인 모델을 직접 실행해 보았습니다:

repos/llama.cpp/build/bin/llama-cli \
-m models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \
-ngl 999 \
-fa on \
-c 4096 \
-n 128

결과:

  • 설정: Gemma 4 26B-A4B Q4, llama.cpp Metal
  • 프롬프트 처리 속도(Prompt tok/s): 298.0
  • 토큰 생성 속도(Generation tok/s): 58.2

초당 58토큰은 빠른 편은 아니지만 사용할 만합니다. 하지만 코딩 에이전트 작업, 특히 에이전트가 수많은 도구 호출(tool calls)을 수행해야 할 때는 속도가 가능한 한 빨라야 합니다.

MTP 드래프트 모델 추가

이제 Gemma 4에 사용할 수 있는 MTP 드래프트 모델이 공개되었습니다: MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf 이 모델은 추측 드래프트 모델(Speculative draft model)로서 llama.cpp에 로드할 수 있습니다:

repos/llama.cpp/build/bin/llama-cli \
-m models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \
--model-draft models/unsloth-gemma-4-26B-A4B-it-GGUF/MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf \
--spec-type draft-mtp \
--spec-draft-n-max 3 \
-ngl 999 \
-fa on \
-c 4096 \
-n 128

4개의 드래프트 토큰을 사용한 MTP 첫 실행 결과는 초당 69.2토큰이었습니다. Unsloth의 'MTP 모델 실행 방법' 가이드에는 다음과 같은 참고 사항이 있습니다: "--spec-draft-n-max 2가 가장 좋은 출발점이라는 것을 발견했습니다. 하지만 성능은 하드웨어에 따라 다르므로 2가 최적의 값이라고 가정하지 마십시오. 1부터 6까지의 값을 시도해보고 시스템에서 가장 빠른 값을 사용하세요."

--spec-draft-n-max 값을 조정해 본 결과, 3개의 드래프트 토큰을 사용했을 때 초당 72.2토큰으로 가장 좋은 결과를 얻었습니다.

  • 메인 모델만 사용 시: 프롬프트 298.0 / 생성 58.2 / 속도 향상비 1.00x
  • 메인 모델 + Q8 MTP 드래프트: 프롬프트 295.6 / 생성 72.2 / 속도 향상비 1.24x

여기서 주목할 만한 점은 프롬프트 처리 속도는 기본적으로 비슷하게 유지되면서, 생성 속도는 약 24% 향상되었다는 것입니다.

MTP 튜닝

--spec-draft-n-max 값을 1부터 6까지 테스트해 보았습니다.

  • 1: 프롬프트 295.5 / 생성 68.4
  • 2: 프롬프트 299.1 / 생성 72.0
  • 3: 프롬프트 295.6 / 생성 72.2
  • 4: 프롬프트 297.3 / 생성 70.7
  • 5: 프롬프트 297.9 / 생성 63.7
  • 6: 프롬프트 296.3 / 생성 61.2

제 M1 Max 머신에서는 3이 가장 빨랐으며, 2도 비슷한 성능을 보여 둘 중 어느 것을 사용해도 괜찮았습니다. 그 이상의 값은 오히려 속도가 느려졌습니다.

MLX 비교

Mac에서 모델을 실행하는 더 빠른 방법이 무엇인지 알아보기 위해 mlx-lm을 통한 MLX 모델도 테스트했습니다.

  • llama.cpp Metal + MTP (Unsloth GGUF Q4 + Q8 MTP): 72.2 tok/s
  • llama.cpp Metal (Unsloth GGUF Q4): 58.2 tok/s
  • MLX-LM (Unsloth UD MLX 4-bit): 45.8 tok/s
  • MLX-LM (mlx-community 4-bit): 43.9 tok/s
  • MLX-LM (mlx-community OptiQ 4-bit): 38.1 tok/s

제 생각에는 Mac에 최적화된 MLX가 가장 빠를 것이라고 예상했습니다. 하지만 이 특정 설정에서는 llama.cpp가 MLX보다 빨랐고, MTP가 결합된 llama.cpp가 확실히 최고의 선택이었습니다. 아마도 오랜 시간에 걸쳐 llama.cpp에 쏟아부은 모든 노력과 최적화 덕분에 현실적인 환경에서도 상당히 잘 최적화된 것이 아닐까 싶습니다.

원문 보기
원문 보기 (영어)
I'd had my internet fail a few times recently leaving me stranded without a coding agent, and so when I saw the "Gemma 4 now runs 2x faster with MTP" Multi-Token Prediction update for Gemma 4 I decided to have a go at getting it running. I wanted a local coding agent setup that: was fast enough to actually use on my Mac worked through an OpenAI compatible API (so I could use it in other tools) and preferably could handle screenshots/images when needed, so I can feed it screenshots of what it has made. And I did! This video is realtime. And shows the agent responding at a perfectly usable speed. After a bit of testing the final setup I ended up with is: llama.cpp built with Metal on macOS Gemma 4 26B-A4B in GGUF format A Q8 MTP draft model for speculative decoding The Gemma 4 multimodal projector Pi as the terminal coding agent This was tested on an Apple M1 Max with 64 GB unified memory, running macOS 15.7.7. The Model The main model is: gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf . Link on Huggingface: models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf That file is about 16 GB. With the MTP draft head and multimodal projector the model folder is about 17 GB. The benchmark prompt was: Write a compact Python function that parses a unified diff and returns the changed file paths. Then explain two edge cases. Each benchmark generated about 128 tokens. Baseline: llama.cpp + Metal First I ran the main model directly through llama.cpp with Metal acceleration: repos/llama.cpp/build/bin/llama-cli \ -m models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \ -ngl 999 \ -fa on \ -c 4096 \ -n 128 Result: Setup Prompt tok/s Generation tok/s Gemma 4 26B-A4B Q4, llama.cpp Metal 298.0 58.2 58 tokens/second is not fast, but is usable, but for coding-agent work you want it to be as fast as possible, especially when the agent is making many tool calls. Adding the MTP Draft Model Gemma 4 now has the MTP draft model available : MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf This can be loaded by llama.cpp as a speculative draft model: repos/llama.cpp/build/bin/llama-cli \ -m models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \ --model-draft models/unsloth-gemma-4-26B-A4B-it-GGUF/MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf \ --spec-type draft-mtp \ --spec-draft-n-max 3 \ -ngl 999 \ -fa on \ -c 4096 \ -n 128 The first run with MTP came in at 69.2 tokens/second using 4 draft tokens. However, Unsloth's guide on How to Run MTP Models includes this note: "We found --spec-draft-n-max 2 is the best starting point however, do not assume 2 is optimal, as performance is hardware-dependent. Try any value from 1 through 6 and use whichever is fastest for your system." After sweeping --spec-draft-n-max , the best result was 72.2 tokens/second with 3 draft tokens. Setup Prompt tok/s Generation tok/s Speedup Main model only 298.0 58.2 1.00x Main model + Q8 MTP draft 295.6 72.2 1.24x The useful part is that prompt processing stayed basically the same, while generation improved by about 24%. Tuning MTP I tested --spec-draft-n-max values from 1 to 6. --spec-draft-n-max Prompt tok/s Generation tok/s 1 295.5 68.4 2 299.1 72.0 3 295.6 72.2 4 297.3 70.7 5 297.9 63.7 6 296.3 61.2 On my M1 Max machine, 3 was the fastest, with 2 close enough that either would be fine. Values above that got slower. MLX Comparison I also tested MLX models through mlx-lm , to find out which is the faster way to run the model on a Mac, llama.cpp or mlx. Runtime Model Generation tok/s llama.cpp Metal + MTP Unsloth GGUF Q4 + Q8 MTP 72.2 llama.cpp Metal Unsloth GGUF Q4 58.2 MLX-LM Unsloth UD MLX 4-bit 45.8 MLX-LM mlx-community 4-bit 43.9 MLX-LM mlx-community OptiQ 4-bit 38.1 I thought MLX (being optimised for the Mac) would be fastest. However, for this specific setup, llama.cpp was faster than MLX, and llama.cpp with MTP was clearly the best option. I guess all the effort and tweaking which has gone into llama.cpp over time means it quite well optimised fr macOS despite being cross platform. I also tried Gemma 4 MTP through gemma-4-swift-mlx , but the tested 26B 4-bit MLX checkpoints did not match the loader's expected weight keys, and I already had the previous MLX tests, so moved on rather than redownload new models and try to tweak things to match. Adding Image Support For Pi, I also wanted to be able to attach screenshots. The local model entry I setup for it originally declared the model as text-only: "input" : [ "text" ] That meant Pi did not send image tool output through to the model properly. The llama.cpp server also needs the Gemma 4 multimodal projector in order for the multi-modal part to work (only the 12B is natively multi-modal ): mmproj-BF16.gguf When loaded with --mmproj , llama.cpp advertises multimodal support, and Pi can send images. I re-ran the text benchmark with the projector loaded, just to check it didn't change the speed: Setup Projector Prompt tok/s Generation tok/s llama.cpp Metal + MTP none 120.3 71.4 llama.cpp Metal + MTP mmproj-BF16.gguf 297.4 72.2 The final run with the projector did not show a text-generation slowdown. Now for setup instructions: Install llama.cpp Install dependencies: brew install cmake git tmux python@3.11 Clone and build llama.cpp: mkdir -p ~/Developer/ML-Models/Gemma4/repos cd ~/Developer/ML-Models/Gemma4 git clone https://github.com/ggml-org/llama.cpp repos/llama.cpp cd repos/llama.cpp cmake -B build \ -DCMAKE_BUILD_TYPE = Release \ -DGGML_METAL = ON \ -DGGML_ACCELERATE = ON cmake --build build --config Release -j The build I tested had: GGML_METAL=ON GGML_ACCELERATE=ON GGML_BLAS=ON GGML_BLAS_VENDOR=Apple Download the Model Files Create a Python environment: cd ~/Developer/ML-Models/Gemma4 python3.11 -m venv .venv source .venv/bin/activate pip install -U huggingface_hub hf_xet Download the files: mkdir -p models/unsloth-gemma-4-26B-A4B-it-GGUF huggingface-cli download unsloth/gemma-4-26B-A4B-it-GGUF \ gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \ mmproj-BF16.gguf \ MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf \ --local-dir models/unsloth-gemma-4-26B-A4B-it-GGUF You should end up with: models/unsloth-gemma-4-26B-A4B-it-GGUF/ gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf mmproj-BF16.gguf MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf Start the Local Server This is the final server command: repos/llama.cpp/build/bin/llama-server \ -m models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \ --model-draft models/unsloth-gemma-4-26B-A4B-it-GGUF/MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf \ --mmproj models/unsloth-gemma-4-26B-A4B-it-GGUF/mmproj-BF16.gguf \ --spec-type draft-mtp \ --spec-draft-n-max 3 \ -ngl 999 \ -fa on \ -c 65536 \ --parallel 1 \ --host 127 .0.0.1 \ --port 8080 The OpenAI-compatible endpoint is: http://127.0.0.1:8080/v1 I used a small start_server.sh wrapper so it runs inside tmux: #!/usr/bin/env bash set -euo pipefail ROOT_DIR= " $(cd " $( dirname "${ BASH_SOURCE [0] }" ) " && pwd) " SESSION_NAME= "${ SESSION_NAME :- gemma4 -server }" HOST= "${ HOST :- 127 .0.0.1 }" PORT= "${ PORT :- 8080 }" CTX_SIZE= "${ CTX_SIZE :- 65536 }" PARALLEL= "${ PARALLEL :- 1 }" LLAMA_SERVER= " $ROOT_DIR /repos/llama.cpp/build/bin/llama-server" MODEL= " $ROOT_DIR /models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf" DRAFT_MODEL= " $ROOT_DIR /models/unsloth-gemma-4-26B-A4B-it-GGUF/MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf" MMPROJ= " $ROOT_DIR /models/unsloth-gemma-4-26B-A4B-it-GGUF/mmproj-BF16.gguf" LOG_FILE= " $ROOT_DIR /logs/llama-server-mtp.log" mkdir -p " $ROOT_DIR /logs" tmux new-session -d -s " $SESSION_NAME " -c " $ROOT_DIR " \ " $LLAMA_SERVER \ -m ' $MODEL ' \ --model-draft ' $DRAFT_MODEL ' \ --mmproj ' $MMPROJ ' \ --spec-type draft-mtp \ --spec-draft-n-max 3 \ -ngl 999 \ -fa on \ -c ' $CTX_SIZE ' \ --parallel ' $PARALLEL ' \ --host ' $HOST ' \ --port ' $PORT ' \ 2>&1 | tee -a ' $LOG_FILE '" Start it: chmod +x start_server.sh ./start_server.sh Check that the server is running: curl http://127.0.0.1:8080/v1/models Configure Pi Pi reads