Gemma 4 12B를 Mac Studio에서 24/7 로컬로 돌리기

TL;DR — 어제(2026-06-03) 공개된 Gemma 4 12B(인코더리스 통합 멀티모달)를 Mac Studio M4 Max / 64GB에서 MLX 기반 OpenAI 호환 서버로 24/7 띄웠다. 텍스트·이미지·오디오 모두 실동작 확인. 평소엔 메모리 ~13GB만 점유하고 추론 시에만 GPU를 100% 당겨 쓴다. 64GB의 절반 이상이 남는다.


1. Gemma 4 12B 소개

Google AI Developers가 X 포스트Gemma 4 12B 출시를 알렸다. 핵심 주장은 “멀티모달 AI를 노트북에서”. 스펙을 정리하면:

📎 공식 자료출시 발표 (X) · Google 공식 블로그 · 개발자 가이드


2. 목표와 장비

목표: Gemma 4 12B를 이 머신에서 항상 떠 있는 OpenAI 호환 로컬 서버로 운용한다. (재부팅·크래시에도 알아서 살아나야 함)

장비 (실측):

항목
모델명 Mac Studio (Mac16,9)
Apple M4 Max (CPU 16코어: 성능 12 + 효율 4)
GPU 40코어, Metal 4
메모리 64 GB 통합메모리(unified)
디스크 477 GB 여유
OS macOS 26.5

통합메모리가 핵심이다. Apple Silicon은 CPU와 GPU가 같은 메모리 풀을 쓰므로, 별도 VRAM 없이 64GB 전체를 모델에 쓸 수 있다. 12B는 여기서 가벼운 짐이다.


3. 어떤 런타임으로 띄울까

Apple Silicon에서 로컬 LLM을 상시 서빙하는 현실적인 선택지는 셋이다.

옵션 운영 난이도 속도 멀티모달 24/7 방식 컨텍스트
Ollama ★ 가장 쉬움 빠름 이미지 O / 오디오 △ brew services(launchd) 128K
MLX ★★ 중간 가장 빠름 이미지 O / 오디오 ✅(후술) LaunchAgent 256K
llama.cpp ★★★ 중상 빠름 이미지 O / 오디오 △ LaunchDaemon 256K

Ollama의 “한 줄이면 끝”은 분명한 장점이지만, 운영을 사람이 직접 하지 않는다면 그 편의성은 의미가 줄어든다. 순수 성능·역량으로만 보면 MLX가 이 M4 Max에서 가장 빠르고, 풀 256K 컨텍스트를 노출하며, Apple 통합메모리를 가장 효율적으로 쓴다. 그래서 MLX로 결정했다.


4. “오디오는 된다 vs 안 된다” — 능력과 도구는 다르다

리서치 중 한 가지가 걸렸다. “MLX는 12B 오디오를 아직 지원 안 할 수 있다”. 이 문장의 정체를 정확히 봐야 한다. 모델 능력런타임 구현은 다른 층이다.

문서를 믿는 대신 직접 돌려봤다. 결론부터: 된다. (§6 참고) 모델은 1일 차였고, README가 옛 문구였을 뿐이다. 실제로 받은 MLX 8-bit 변환본의 config.jsonaudio_config·audio_token_id가, processor_config.jsonGemma4UnifiedAudioFeatureExtractor(16kHz, mel 128)가 모두 들어 있었고, mlx-vlmgemma4_unified 모듈 소스에도 embed_audio, get_audio_features()가 배선돼 있었으며 CLI엔 --audio 플래그가 노출돼 있었다.

교훈: 새 모델에서 “지원 안 함” 같은 문구는 도구가 아직 못 따라온 것일 때가 많다. 확인 가능한 건 확인하면 된다.


5. 설치

5-1. 격리 환경 (Python 3.12)

Python 3.14는 너무 최신이라 MLX 계열 휠이 없을 수 있어, uv로 3.12 격리 환경을 만들었다.

cd ~/workspace/seapy/gemma4
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -U \
    mlx mlx-lm mlx-vlm huggingface_hub librosa soundfile pillow

설치된 버전(실측): mlx 0.31.2 · mlx-lm 0.31.3 · mlx-vlm 0.6.1 · transformers 5.10.1 · huggingface-hub 1.17.0.

mlx-vlm의 모델 모듈을 확인하면 gemma4_unified 전용 구현이 있다 — 로딩은 확실히 된다.

gemma*: ['gemma3', 'gemma3n', 'gemma4', 'gemma4_unified', 'paligemma']

5-2. 모델 다운로드 (게이트 없음)

hf download mlx-community/gemma-4-12B-it-8bit   # ~12GB, safetensors 3 shard

8-bit를 택했다. 64GB라 품질 우선이 합리적이고(~12.7GB), 그래도 50GB가 남는다. 더 빠른 속도를 원하면 4-bit(~7GB, ~40–50 tok/s), 최고 품질이면 BF16(~24GB)도 충분히 들어간다.


6. 동작 검증 — 텍스트·이미지·오디오 실측

모델을 한 번 로드해 세 가지 모달리티를 모두 돌렸다. (temperature 0.0)

① 텍스트

python -m mlx_vlm.generate --model mlx-community/gemma-4-12B-it-8bit \
  --prompt "What is the capital of France?"
# → "The capital of France is Paris."   (32.3 tok/s, peak 12.8GB)

② 이미지 (벌이 분홍 꽃에 앉은 사진)

python -m mlx_vlm.generate --model mlx-community/gemma-4-12B-it-8bit \
  --image bee.jpg --prompt "Describe this image."
# → "A close-up shot shows a bumblebee on a pink flower. ... The lighting is
#    soft and natural, creating a peaceful and serene atmosphere."   (32.9 tok/s)

정확히 호박벌 + 분홍 꽃을 잡아냈다.

③ 오디오 (macOS say로 만든 음성 → 16kHz wav)

python -m mlx_vlm.generate --model mlx-community/gemma-4-12B-it-8bit \
  --audio g4_audio.wav --prompt "Transcribe the spoken words."
# 입력 음성: "The quick brown fox ... the capital of France is Paris,
#            and today the weather in Seoul is clear."
# → "토키 브라운 폭스 점프스 오버 더 레이지 도그 더 캐피탈 오버 프랑스
#    이즈 파리 엔투데이 더 웨더 인 서울 이즈 클리어"   (33.5 tok/s)

오디오 내용을 정확히 알아들었다. 다만 언어 미지정 + greedy 디코딩 탓에 출력이 한글 음차로 떨어졌는데, "Transcribe in English"처럼 지정하면 로마자로 나온다. 즉 텍스트·이미지·오디오 3종 모두 이 머신에서 실동작 확정.

생성 속도

속도는 세 모달리티 모두 ~32–33 tok/s로 일관됐고, prefill(입력 처리)은 276–442 tok/s로 빨랐다.


7. 24/7 상주 서버 만들기

7-1. OpenAI 호환 서버

mlx_vlm.server는 OpenAI 호환 API를 그대로 제공한다(텍스트+이미지+오디오).

python -m mlx_vlm.server \
  --model mlx-community/gemma-4-12B-it-8bit \
  --host 127.0.0.1 --port 8080
# → http://127.0.0.1:8080/v1  (/v1/models, /v1/chat/completions)

thinking 모드는 기본 off라 출력이 깔끔하고, 요청별로 enable_thinking을 켜면 추론을 노출할 수 있다.

구조

7-2. launchd LaunchAgent로 데몬화

Apple Silicon에서 MLX는 GPU(Metal)를 쓰므로 로그인 세션이 필요하다 → LaunchDaemon이 아니라 LaunchAgent가 맞다. ~/Library/LaunchAgents/com.seapy.gemma4.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>com.seapy.gemma4</string>
  <key>ProgramArguments</key><array>
    <string>/Users/seapy/workspace/seapy/gemma4/.venv/bin/python</string>
    <string>-m</string><string>mlx_vlm.server</string>
    <string>--model</string><string>mlx-community/gemma-4-12B-it-8bit</string>
    <string>--host</string><string>127.0.0.1</string>
    <string>--port</string><string>8080</string>
  </array>
  <key>EnvironmentVariables</key><dict>
    <key>HOME</key><string>/Users/seapy</string>
    <key>PATH</key><string>/Users/seapy/workspace/seapy/gemma4/.venv/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>ProcessType</key><string>Interactive</string>
  <key>StandardOutPath</key><string>/Users/seapy/Library/Logs/gemma4-server.log</string>
  <key>StandardErrorPath</key><string>/Users/seapy/Library/Logs/gemma4-server.err.log</string>
</dict></plist>

HOME을 꼭 넣어야 한다. 안 그러면 ~/.cache/huggingface의 모델을 못 찾는다. launchd는 쉘 rc를 읽지 않으므로 환경변수는 plist에 직접 적는다.

등록·관리:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.seapy.gemma4.plist  # 시작
launchctl print     gui/$(id -u)/com.seapy.gemma4 | grep -E 'state|pid'         # 상태
launchctl kickstart -k gui/$(id -u)/com.seapy.gemma4                            # 재시작
launchctl bootout   gui/$(id -u)/com.seapy.gemma4                               # 정지

7-3. 크래시 자동복구 검증

말로만 “KeepAlive”가 아니라 실제로 죽여봤다.

current pid: 85143
kill -9 85143
→ AUTO-RECOVERED, new pid: 85425   # launchd가 새 PID로 즉시 재기동

부팅 자동시작 + 크래시 복구 모두 확인. Mac Studio는 능동 냉각이라 24/7 연속 추론에도 스로틀링이 없고, pmset sleep 0으로 잠들지 않게 돼 있다.


8. OpenAI 호환 API로 쓰기

기존 OpenAI SDK/클라이언트를 그대로 붙인다.

from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="local")  # 키는 아무 값
r = client.chat.completions.create(
    model="mlx-community/gemma-4-12B-it-8bit",
    messages=[{"role": "user", "content": "안녕, 뭐 할 수 있어?"}],
)
print(r.choices[0].message.content)

이미지는 image_url 블록(원격 URL 또는 base64 data URI)으로 넘기면 된다. Continue·Cline 등 OpenAI 호환 클라이언트는 base URL만 바꾸면 바로 붙는다.


9. 자원 사용량 — 지금 얼마나 쓰고, 얼마나 남나

상시 떠 있는 서버가 실제로 자원을 얼마나 먹는지 idle/추론중 양쪽을 측정했다.

자원 Idle (상주만) 추론 중 (실측)
메모리 ~12.5 GB 상주 ~14 GB (peak), Metal alloc 17 GB
GPU ~0% 99–100% 점유 (40코어 풀가동)
CPU 0.3% ~22% (1코어분; MLX는 GPU 바운드)
속도 33.1 tok/s

평소엔 메모리 ~14GB만 점유하고 CPU·GPU는 거의 0. 요청이 오는 순간만 GPU를 풀로 당겨 쓰고 끝나면 idle로 돌아간다.

자원 사용

시스템 전체 (64GB):

지표 해석
메모리 여유 86% free 압력 정상
스왑 0 B 페이징 전혀 없음
Load avg 3.89 / 16코어 ~24%, 한가함
발열/스로틀 제한 없음 18초 풀로드에도 스로틀 0

top은 “62G used, 1G unused”로 보이지만, 이건 macOS가 빈 RAM을 캐시로 채우는 정상 동작이다. 실제 지표는 memory_pressure 86% free + swap 0 — 재확보 가능한 inactive 캐시가 ~27GB라 실여유는 넉넉하다.

메모리

헤드룸 결론: 12B는 64GB 중 ~22%(14GB)만 쓴다. 30GB+ 여유 → 12B를 한두 개 더 띄우거나, 26B MoE로 업그레이드하거나, 컨텍스트를 훨씬 크게 잡아도 된다. 병목은 메모리가 아니라 GPU 시간(동시 요청은 GPU를 시분할)이다.


10. 마치며

어제 나온 12B 멀티모달 모델이, 데스크톱급 Mac에서 완전 로컬로 · 24/7 돌아간다. 정리하면:

환경: Mac Studio M4 Max · 64GB · macOS 26.5 / Gemma 4 12B (mlx-community 8-bit) · mlx-vlm 0.6.1 · 측정일 2026-06-04

부록: 관리 치트시트

# 상태
launchctl print gui/$(id -u)/com.seapy.gemma4 | grep -E 'state|pid'
# 빠른 점검
curl -s http://127.0.0.1:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"mlx-community/gemma-4-12B-it-8bit","messages":[{"role":"user","content":"ping"}],"max_tokens":5}'
# 정지 / 시작 / 재시작
launchctl bootout   gui/$(id -u)/com.seapy.gemma4
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.seapy.gemma4.plist
launchctl kickstart -k gui/$(id -u)/com.seapy.gemma4
Thu, 04 Jun 2026 10:00:00 +0900