통합 메모리와 지연 실행으로 이해하는 Apple의 머신러닝 프레임워크
MLX란
MLX는 Apple의 머신러닝 연구 조직이 공개한 배열 프레임워크다. Python API의 기본 모양은 NumPy와 가깝고, `mlx.nn`과 `mlx.optimizers`는 신경망 모델과 최적화기를 구성한다. Python 외에도 C++, C, Swift API가 있다.
MLX의 핵심 특성은 다음과 같다.
- Apple Silicon의 통합 메모리를 활용한다.
- CPU와 GPU 연산이 같은 배열 저장 공간을 사용한다.
- 연산을 즉시 실행하지 않는 지연 평가를 사용한다.
- 자동미분, 자동 벡터화, 그래프 컴파일을 함수 변환으로 조합한다.
- 계산 그래프가 동적으로 만들어진다.
- 신경망 계층, 손실 함수, 최적화기를 제공한다.
- MLX LM을 통해 Hugging Face 계열 LLM의 추론, 양자화, LoRA 학습을 지원한다.
공식 문서 기준으로 macOS에서 PyPI 패키지를 사용하려면 Apple Silicon, 네이티브 arm64 Python 3.10 이상, macOS 14 이상이 필요하다. MLX 프로젝트에는 Linux CPU와 CUDA 백엔드 패키지도 안내되어 있지만, 이 글은 MLX의 대표적인 사용 환경인 Apple Silicon 맥에 초점을 맞춘다.
Apple Silicon의 통합 메모리
전통적인 외장 GPU 시스템에서는 CPU 메모리와 GPU 메모리가 물리적으로 분리되어 있다. 프레임워크가 텐서를 CPU에서 GPU로 복사하고 다시 가져오는 과정이 필요하다.
Apple Silicon은 CPU와 GPU가 하나의 통합 메모리 풀에 접근한다. MLX 배열은 특정 장치 메모리에 소속된 것으로 만들지 않는다. 배열은 통합 메모리에 있고, 어느 장치가 연산할지를 연산 또는 stream에서 정한다.
import mlx.core as mx
a = mx.random.normal((1024, 1024))
b = mx.random.normal((1024, 1024))
cpu_result = mx.add(a, b, stream=mx.cpu)
gpu_result = mx.add(a, b, stream=mx.gpu)
mx.eval(cpu_result, gpu_result)두 결과는 같은 입력 배열을 사용하지만 하나는 CPU, 다른 하나는 GPU stream에서 계산한다. CPU 결과를 GPU 연산이 사용하면 MLX 스케줄러가 의존성을 추적해 필요한 순서를 맞춘다.
통합 메모리가 복사를 모두 없애고 메모리를 무한하게 만드는 것은 아니다. 모델 가중치, KV 캐시, 활성값, 그래디언트, 운영체제와 다른 앱이 같은 물리 메모리를 공유한다. 메모리 압축과 swap이 시작되면 속도가 크게 떨어질 수 있다.
설치와 환경 확인
프로젝트별 가상환경에서 설치한다.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install mlx현재 Python이 Rosetta의 x86_64가 아니라 arm64인지 확인한다.
python -c 'import platform; print(platform.machine())'
uname -m둘 다 일반적으로 `arm64`여야 한다. 지원되는 macOS와 Python인데 설치 파일을 찾지 못한다면 x86 Python을 실행 중인지 먼저 확인한다.
설치 뒤 간단히 확인한다.
import mlx.core as mx
x = mx.arange(6).reshape(2, 3)
print(x)
print(mx.sum(x))LLM 도구를 사용하려면 별도 패키지를 설치한다.
python -m pip install mlx-lm프로젝트에서 재현 가능한 버전이 필요하면 테스트한 버전을 의존성 파일과 잠금 파일에 기록한다. 최신 버전으로 올릴 때 생성 결과, 성능, 메모리 사용량을 다시 측정한다.
mlx.core 배열
핵심 모듈은 `mlx.core`, 관례적인 별칭은 `mx`다.
import mlx.core as mx
x = mx.array([[1.0, 2.0], [3.0, 4.0]])
y = mx.ones((2, 2))
z = x @ y
print(z.shape)
print(z.dtype)배열 생성, reshape, transpose, 브로드캐스팅, 축소, 선형대수, 난수 같은 연산의 이름이 NumPy와 비슷하다. 문법이 비슷해도 NumPy 배열과 MLX 배열은 다른 타입이고 실행 모델도 다르다.
import numpy as np
import mlx.core as mx
np_array = np.array([1, 2, 3], dtype=np.float32)
mx_array = mx.array(np_array)
back_to_numpy = np.array(mx_array)MLX 배열을 NumPy로 변환하거나 출력하면 계산이 평가될 수 있다. 반복 학습 루프 안에서 매 단계 NumPy 변환을 하면 동기화와 복사 비용이 생긴다.
지연 평가
MLX 연산은 호출 즉시 계산되지 않는다. 연산 관계를 그래프로 기록하고 값이 필요할 때 실행한다.
import mlx.core as mx
x = mx.ones((1024, 1024))
y = x * 2
z = mx.sum(y)
# 여기까지는 그래프를 구성한다.
mx.eval(z) # 실제 계산을 강제한다.배열을 출력하거나 NumPy로 바꾸거나 scalar의 `.item()`을 호출하면 암시적으로 평가된다. 저장 함수도 값을 실제로 만들어야 하므로 평가한다.
지연 평가는 사용하지 않는 결과를 계산하지 않고, 자동미분과 그래프 최적화를 적용하기 쉽게 한다. 반대로 그래프를 너무 오래 쌓으면 그래프 관리 비용과 메모리가 늘어난다.
학습에서는 한 iteration의 순전파, 역전파, 최적화기 갱신을 만든 뒤 평가하는 위치가 자연스럽다.
for x, y in batches:
loss, grads = loss_and_grad(model, x, y)
optimizer.update(model, grads)
mx.eval(loss, model.parameters(), optimizer.state)loss를 먼저 `print(loss)`하면 순전파만 부분 평가된 뒤 다시 갱신 계산이 이어질 수 있다. 측정 위치가 실행 시점에 영향을 준다는 점을 기억한다.
자동미분
`mx.grad`는 스칼라 출력 함수의 그래디언트를 계산하는 함수를 만든다. 값과 그래디언트를 함께 얻으려면 `mx.value_and_grad` 또는 모델용 `nn.value_and_grad`를 사용한다.
import mlx.core as mx
def loss_fn(w):
return mx.sum((w - 3.0) ** 2)
w = mx.array([0.0, 1.0, 2.0])
grad_fn = mx.grad(loss_fn)
grad = grad_fn(w)
mx.eval(grad)
print(grad)이 예에서 미분 결과는 각 원소에 대해 `2 * (w - 3)`이다. MLX는 Python 코드를 문자열로 미분하는 것이 아니라 배열 연산으로 구성된 계산 그래프에 변환을 적용한다.
모델 파라미터가 중첩 딕셔너리와 리스트 구조를 가지더라도 MLX의 tree utility와 `nn.Module`이 구조를 유지한 채 그래디언트를 다룬다.
vmap으로 배치 계산하기
`mx.vmap`은 원소 하나를 처리하는 함수를 배치 차원에 자동으로 적용한다.
import mlx.core as mx
def score(x, w):
return mx.sum(x * w)
batched_score = mx.vmap(score, in_axes=(0, None))
x = mx.array([[1.0, 2.0], [3.0, 4.0]])
w = mx.array([0.5, 2.0])
result = batched_score(x, w)`x`의 0번 축을 배치로 보고 같은 `w`를 모든 예에 사용한다. Python for문으로 하나씩 호출하는 대신 프레임워크가 벡터화된 그래프를 만든다.
모든 함수가 무조건 vmap에 적합한 것은 아니다. 배열 데이터에 따라 Python 제어 흐름이 달라지거나 외부 부수 효과가 있으면 변환하기 어렵다.
mx.compile
`mx.compile`은 반복 실행하는 계산 그래프의 공통 작업을 줄이고 일부 연산을 융합해 실행 시간과 메모리를 개선할 수 있다.
import mlx.core as mx
@mx.compile
def normalize(x):
return (x - mx.mean(x)) / (mx.std(x) + 1e-6)학습 step 전체를 컴파일할 때는 모델과 최적화기의 변경 상태를 입력과 출력 capture에 포함해야 한다. Dropout처럼 난수를 사용한다면 `mx.random.state`도 상태에 포함한다.
컴파일은 항상 빠르다는 보장이 없다. 함수의 입력 구조와 shape가 계속 달라지면 캐시가 늘거나 컴파일 비용이 이득을 상쇄할 수 있다. 먼저 일반 지연 실행으로 정확성을 확인하고 프로파일링한 반복 구간에 적용한다.
mlx.nn으로 모델 만들기
`mlx.nn.Module`을 상속해 계층을 멤버로 두고 `__call__`에서 순전파를 정의한다.
import mlx.core as mx
import mlx.nn as nn
class MLP(nn.Module):
def __init__(self, input_dim: int, hidden_dim: int, classes: int):
super().__init__()
self.layers = [
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, classes),
]
def __call__(self, x):
for layer in self.layers:
x = layer(x)
return x
model = MLP(32, 64, 10)
x = mx.random.normal((8, 32))
logits = model(x)
mx.eval(logits)Module은 파라미터, 학습 가능 여부, 저장과 로딩을 관리한다. 계층을 일반 Python 목록에 넣어도 MLX Module이 중첩 구조를 추적할 수 있는지 사용하는 버전의 문서를 확인하고, `model.parameters()`에서 의도한 계층이 모두 보이는지 검사한다.
학습과 평가에서 동작이 다른 Dropout, BatchNorm 같은 계층은 `model.train()`과 `model.eval()` 상태를 올바르게 전환한다.
간단한 학습 루프
다중 클래스 분류기의 최소 학습 구조는 다음과 같다.
import mlx.core as mx
import mlx.nn as nn
import mlx.optimizers as optim
model = MLP(32, 64, 10)
optimizer = optim.Adam(learning_rate=1e-3)
def loss_fn(model, x, y):
logits = model(x)
return mx.mean(nn.losses.cross_entropy(logits, y))
loss_and_grad = nn.value_and_grad(model, loss_fn)
for step, (x, y) in enumerate(train_batches):
loss, grads = loss_and_grad(model, x, y)
optimizer.update(model, grads)
mx.eval(loss, model.parameters(), optimizer.state)
if step % 100 == 0:
print(step, loss.item())핵심 순서는 순전파와 loss, 그래디언트, optimizer update, `mx.eval`이다. 모델 파라미터만 평가하고 optimizer state를 빼면 모멘텀 같은 상태 갱신이 기대한 시점에 실행되지 않을 수 있다.
학습 데이터는 작은 Python 객체를 반복해서 만드는 것보다 적절한 배치 배열로 묶는다. 입력 shape와 dtype이 계속 바뀌면 컴파일과 메모리 재사용 효율이 떨어질 수 있다.
dtype과 정밀도
모델 가중치와 활성값 정밀도는 메모리와 속도, 수치 안정성에 영향을 준다. float32는 안정적이지만 메모리를 더 사용한다. float16과 bfloat16은 메모리를 줄이고 GPU 연산에 유리할 수 있으나 손실과 축소 연산에서 정밀도 문제가 생길 수 있다.
정밀도를 낮출 때 확인할 항목은 다음과 같다.
- loss가 NaN 또는 inf가 되는가
- 그래디언트가 지나치게 작거나 커지는가
- 검증 정확도가 float32 기준에서 얼마나 달라지는가
- 메모리 피크와 처리량이 실제로 개선되는가
dtype을 바꾸는 것과 LLM의 4비트 양자화는 같은 개념이 아니다. 저비트 양자화는 가중치를 그룹 단위 스케일과 함께 압축해 저장·계산하는 별도의 표현이다.
모델 저장과 로딩
학습 재현에는 모델 가중치뿐 아니라 설정, tokenizer, optimizer state, step, 난수 seed, 데이터 버전을 함께 기록한다.
MLX는 safetensors와 npz 같은 형식의 저장 API를 제공한다. 정확한 함수와 Module 가중치 형식은 사용하는 MLX 버전 문서를 따른다.
model.save_weights("model.safetensors")
restored = MLP(32, 64, 10)
restored.load_weights("model.safetensors")
mx.eval(restored.parameters())로드 뒤에는 테스트 입력으로 출력 shape와 기준 결과를 확인한다. 클래스 구조나 파라미터 이름을 바꿨다면 누락되거나 남는 가중치를 조용히 무시하지 않도록 strict 로딩 여부를 확인한다.
MLX LM
MLX LM은 MLX 위에서 대규모 언어 모델을 생성하고 미세조정하는 Python 패키지다. Hugging Face Hub의 호환 모델이나 로컬 변환 모델을 사용할 수 있다.
설치와 간단한 생성은 다음과 같다.
python -m pip install mlx-lm
mlx_lm.generate \
--model mlx-community/Mistral-7B-Instruct-v0.3-4bit \
--prompt "통합 메모리를 한 문단으로 설명해줘" \
--max-tokens 200모델 이름은 예시다. 모델 카드의 라이선스, 입력 형식, 지원 언어, 메모리 요구량을 확인한다. 저장소 이름에 4bit가 있다고 해서 모든 모델이 같은 품질과 속도를 내는 것은 아니다.
Python API에서는 모델과 tokenizer를 로드한 뒤 chat template을 적용하고 생성한다.
from mlx_lm import generate, load
model, tokenizer = load("MODEL_OR_LOCAL_PATH")
messages = [{"role": "user", "content": "MLX의 장점을 설명해줘"}]
prompt = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=False,
)
response = generate(
model,
tokenizer,
prompt=prompt,
max_tokens=200,
)
print(response)모델마다 chat template 요구가 다르다. 단순 문자열을 임의 형식으로 조합하지 말고 tokenizer의 template과 모델 카드를 따른다.
KV 캐시와 긴 문맥
자동회귀 LLM은 이전 토큰의 key와 value를 KV 캐시에 보관해 다음 토큰마다 앞부분을 다시 계산하지 않는다. 문맥이 길고 배치가 커질수록 캐시 메모리가 커진다.
MLX LM은 KV 캐시 양자화 옵션을 제공한다. 메모리를 줄일 수 있지만 정확도, 속도, batching 지원에 제약이 있을 수 있다. 공식 서버 문서는 양자화된 KV 캐시에서 batching이 지원되지 않고 요청을 하나씩 처리한다고 안내한다.
긴 문맥 문제가 생기면 다음 순서로 본다.
- 실제 입력 토큰 수를 측정한다.
- 필요 없는 대화 기록과 문서를 줄인다.
- 모델 크기와 양자화 수준을 조정한다.
- KV 캐시 양자화와 prefill 크기를 시험한다.
- 생성 품질과 처리량을 함께 비교한다.
문맥 창 최대값까지 항상 채우는 것이 좋은 검색이나 답변을 보장하지 않는다.
양자화
양자화는 모델 가중치의 비트 수를 줄여 메모리와 저장 공간을 줄인다. MLX LM의 convert 명령으로 호환 Hugging Face 모델을 변환하고 양자화할 수 있다.
mlx_lm.convert \
--hf-path MODEL_REPOSITORY \
--mlx-path ./mlx-model \
-qCLI 옵션은 버전별로 바뀔 수 있으므로 실행 전 `mlx_lm.convert --help`를 확인한다.
낮은 비트는 더 큰 모델을 제한된 메모리에 올릴 수 있게 하지만 품질 손실과 연산 특성 변화가 있다. 모델 크기 하나만 보고 고르지 말고 실제 업무 프롬프트 평가셋으로 비교한다.
LoRA와 QLoRA
LoRA는 기본 모델 전체를 변경하지 않고 선형 계층에 작은 저랭크 어댑터를 학습한다. 기본 모델이 양자화되어 있으면 QLoRA 방식으로 메모리를 더 줄일 수 있다.
현재 MLX LM의 기본 명령 구조는 다음과 같다.
mlx_lm.lora \
--model MODEL_OR_PATH \
--train \
--data ./data \
--iters 600로컬 데이터 디렉터리에는 학습용 `train.jsonl`, 선택적인 `valid.jsonl`, 평가용 `test.jsonl`을 둘 수 있다. chat 데이터는 다음과 같은 한 줄 JSON 형식을 사용한다.
{"messages":[
{"role":"system","content":"간결하게 답하세요."},
{"role":"user","content":"포워드 프록시란?"},
{"role":"assistant","content":"클라이언트를 대신해 외부 서버에 접속하는 중계 서버입니다."}
]}실제 JSONL은 한 레코드를 물리적인 한 줄에 저장한다. 위 예시는 읽기 위해 줄을 나누어 표시했다.
학습된 어댑터로 생성한다.
mlx_lm.generate \
--model MODEL_OR_PATH \
--adapter-path ./adapters \
--prompt "질문"메모리가 부족하면 양자화 모델, batch 크기, 학습할 LoRA 계층 수, sequence 길이를 줄인다. 학습 loss가 내려간 사실만으로 일반화가 확인되지 않는다. 분리된 검증·테스트 데이터와 실제 질문 평가셋을 사용한다.
로컬 HTTP 서버
MLX LM에는 OpenAI Chat API와 비슷한 HTTP 서버가 있다.
mlx_lm.server --model MODEL_OR_PATH기본 로컬 서버에 요청하는 예시는 다음과 같다.
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"messages": [{"role": "user", "content": "MLX란?"}],
"temperature": 0.2,
"max_tokens": 200
}'공식 문서는 이 서버가 기본적인 보안 검사만 구현하므로 production 용도로 권장하지 않는다고 명시한다. 인터넷에 직접 바인딩하지 않는다.
운영 서비스에 연결하려면 앞단 API에서 다음을 추가한다.
- 사용자 인증과 권한
- 입력 길이와 파일 크기 제한
- 동시 요청 수와 대기열
- 요청 timeout과 취소
- 모델·어댑터 허용 목록
- 로그의 프롬프트와 개인정보 마스킹
- 출력 스키마 검증
- 상태 확인과 모델 readiness
맥 모델 서버는 루프백 또는 WireGuard 주소에만 바인딩하고, IDC Nginx나 애플리케이션이 암호화된 전용 경로로 호출하게 한다.
prompt cache
여러 요청이 같은 긴 system prompt나 문서 앞부분을 공유하면 prompt cache를 재사용해 prefill 계산을 줄일 수 있다. MLX LM은 캐시를 만들고 저장·로드하는 API 예제를 제공한다.
캐시는 모델, tokenizer, 프롬프트 토큰, 양자화 설정과 맞아야 한다. 사용자별 대화가 섞이지 않도록 캐시 키와 수명을 분리한다. 비밀이나 개인정보가 들어간 캐시 파일의 저장 권한과 삭제 정책도 필요하다.
메모리 계획
맥의 통합 메모리는 모델 전용 VRAM이 아니다. 다음 항목이 함께 사용한다.
모델 가중치
KV 캐시
활성값과 임시 버퍼
LoRA 학습 그래디언트·최적화기 상태
Python과 앱 메모리
운영체제와 화면 메모리활동 모니터에서 메모리 압력과 swap을 보고, MLX의 peak memory 통계와 처리량을 함께 기록한다. 모델이 로드된다는 사실과 안정적으로 서비스할 수 있다는 사실은 다르다. 동시 요청과 최대 문맥에서 메모리 피크를 시험한다.
메모리 부족 대응 순서는 보통 다음과 같다.
- 더 작은 모델을 선택한다.
- 가중치를 양자화한다.
- 입력 문맥과 생성 길이를 줄인다.
- batch와 동시 요청 수를 줄인다.
- KV 캐시를 조정한다.
- 학습에서는 LoRA 계층 수와 batch를 줄인다.
- 다른 GPU 사용 앱을 종료한다.
성능 측정
LLM 성능은 하나의 숫자로 설명되지 않는다.
| 지표 | 의미 |
|---|---|
| prompt tokens/s | 입력 문맥을 처리하는 prefill 속도 |
| generation tokens/s | 출력 토큰을 한 개씩 생성하는 속도 |
| first-token latency | 사용자가 첫 응답을 받기까지의 시간 |
| peak memory | 최대 통합 메모리 사용량 |
| queue time | 동시 요청으로 기다린 시간 |
짧은 프롬프트에서 generation tokens/s만 비교하면 긴 문서 서비스의 prefill 비용을 놓친다. 실제 입력 길이 분포와 동시 요청으로 측정한다.
첫 실행에는 모델 다운로드, 파일 캐시, Metal 커널 준비 등 준비 비용이 포함될 수 있다. 워밍업 뒤 반복 측정하고, 모델 버전·MLX 버전·macOS·전원 모드·메모리 상태를 함께 기록한다.
CPU와 GPU를 나누어 쓰기
MLX는 연산별 stream을 지정할 수 있지만 모든 연산을 사람이 수동 배치하는 것이 항상 빠르지는 않다. 장치 사이 복사 부담은 줄어도 CPU와 GPU가 같은 메모리 대역폭을 공유하고, 의존성 동기화 비용이 있다.
작은 전처리나 제어는 CPU, 큰 행렬 연산은 GPU가 자연스러울 수 있다. 기본 동작으로 기준 성능을 만든 뒤 병목이 확인된 연산만 stream을 바꿔 비교한다.
CPU와 GPU에서 독립적인 연산은 병렬로 실행될 수 있다. 한쪽 결과가 다른 쪽 입력이면 MLX가 의존성을 삽입하므로 무조건 겹쳐 실행되지는 않는다.
분산 실행
MLX는 여러 프로세스와 머신에 걸친 distributed communication을 지원한다. 공식 문서는 MPI, TCP 기반 RING, Thunderbolt RDMA를 이용하는 JACCL 등 여러 백엔드를 설명한다. CUDA 환경에는 NCCL 백엔드도 있다.
import mlx.core as mx
world = mx.distributed.init()
x = mx.distributed.all_sum(mx.ones(10))
print(world.rank(), world.size(), x)프로세스가 하나면 분산 연산은 사실상 no-op로 동작할 수 있어 같은 코드 경로를 유지하기 쉽다. 학습에서는 각 프로세스의 그래디언트를 평균 낸 뒤 optimizer를 갱신한다.
분산이 단일 맥보다 항상 빠르지는 않다. 모델과 batch가 작으면 통신 비용이 더 크다. Thunderbolt 연결, 네트워크, 프로세스 배치, shard 전략을 실제 모델로 측정한다.
PyTorch와의 차이
MLX는 PyTorch와 닮은 상위 API를 제공하지만 동일한 프레임워크가 아니다.
| 항목 | MLX | PyTorch |
|---|---|---|
| 주 사용 강점 | Apple Silicon 로컬 연구와 추론 | 폭넓은 GPU·서버 생태계 |
| 메모리 모델 | Apple Silicon 통합 메모리 중심 | 장치별 텐서 이동이 일반적 |
| 실행 | 지연 평가와 명시적 eval | 일반적으로 eager 실행 중심 |
| 생태계 | 빠르게 성장 중 | 매우 큰 모델·도구 생태계 |
| 배포 | 맥 로컬 앱과 연구에 유리 | 클라우드·CUDA 배포 선택지가 넓음 |
PyTorch 코드를 import 이름만 바꾸어 그대로 실행할 수 있다고 생각하면 안 된다. 데이터 로더, 모델 계층, 저장 형식, 분산 도구, custom operation의 차이를 확인해야 한다.
CUDA 전용 연산이나 특정 학습 프레임워크에 의존한다면 PyTorch가 맞을 수 있다. Apple Silicon에서 로컬 모델을 빠르게 실험하고 Swift 앱까지 연결하려면 MLX가 매력적이다.
MLX Swift
MLX Swift는 Swift에서 MLX 배열과 신경망 API를 사용할 수 있게 한다. Xcode 프로젝트에 패키지 의존성을 추가하고 `MLX`, `MLXNN`, `MLXOptimizers`, `MLXRandom` 등을 선택할 수 있다.
로컬 모델을 Python 서버로 띄우지 않고 macOS 또는 iOS 앱 프로세스 안에서 실행하려는 경우 Swift API를 검토할 수 있다. 앱 번들 크기, 모델 다운로드, 메모리 경고, 백그라운드 실행, 지원 장치 범위를 별도로 설계해야 한다.
SwiftPM 명령줄만으로는 Metal shader의 최종 빌드에 제한이 있어 공식 문서가 안내하는 Xcode 빌드 구성을 확인한다.
재현성과 난수
학습 seed 하나만 기록해도 모든 실행이 완전히 같아지는 것은 아니다. 데이터 순서, 병렬 실행, 연산 구현, MLX와 macOS 버전이 결과에 영향을 줄 수 있다.
최소한 다음을 기록한다.
- MLX와 MLX LM 버전
- Python과 macOS 버전
- 맥 칩과 통합 메모리 용량
- 모델 저장소 revision 또는 파일 해시
- tokenizer와 chat template
- 양자화 설정
- seed, batch, 학습률, sequence 길이
- 데이터 버전과 전처리 코드
`mx.compile` 안에서 난수를 사용하면 random state를 컴파일 상태에 포함해야 한다. 그렇지 않으면 기대하지 않은 재사용 또는 상태 처리가 생길 수 있다.
보안과 라이선스
로컬 실행은 프롬프트가 외부 AI API로 전송되지 않는다는 장점이 있다. 그러나 로컬 서비스도 안전이 자동 보장되지는 않는다.
- 모델 서버를 인터넷에 직접 공개하지 않는다.
- 다운로드한 모델 저장소의 custom code 실행 옵션을 신중히 검토한다.
- 모델과 데이터셋의 라이선스를 확인한다.
- 프롬프트와 응답 로그에 개인정보를 남기지 않는다.
- 모델 출력으로 셸, SQL, 파일 작업을 자동 실행하지 않는다.
- Hugging Face 토큰과 저장소 키를 소스에 넣지 않는다.
- 어댑터에도 학습 데이터가 기억될 가능성을 평가한다.
특히 도구 호출 LLM은 출력 JSON을 스키마로 검증하고 서버의 허용 목록을 거쳐야 한다. 모델은 권한 엔진이 아니다.
언제 MLX를 선택할까
MLX가 잘 맞는 경우는 다음과 같다.
- Apple Silicon 맥에서 로컬 LLM을 실행한다.
- 통합 메모리를 활용해 VRAM보다 큰 모델을 시험한다.
- LoRA 또는 QLoRA로 작은 맞춤 학습을 한다.
- Python 연구 코드를 Swift 앱으로 이어가고 싶다.
- 외부 API에 데이터를 보내지 않는 로컬 처리가 필요하다.
- NumPy와 PyTorch에 가까운 API로 실험하고 싶다.
다른 선택이 나은 경우도 있다.
- CUDA 전용 라이브러리와 커널이 필수다.
- 대규모 멀티 GPU 학습 생태계가 핵심이다.
- Linux 서버 배포가 주 환경이고 MLX 백엔드 검증 부담을 원하지 않는다.
- 특정 PyTorch 확장과 모델 코드가 강하게 결합되어 있다.
- 여러 운영체제에서 동일한 실행 결과와 지원 범위가 필요하다.
실무 시작 순서
- 네이티브 arm64 Python 가상환경에 MLX를 설치한다.
- 작은 배열 연산으로 지연 평가와 `mx.eval`을 익힌다.
- 간단한 `nn.Module`과 학습 루프를 실행한다.
- 실제 모델의 메모리 피크와 속도를 측정한다.
- MLX LM으로 작은 양자화 모델을 로컬 생성한다.
- 업무 평가셋으로 모델 품질을 확인한다.
- 필요할 때만 LoRA 학습과 `mx.compile`을 추가한다.
- 서비스 앞에 인증, 큐, timeout, 요청 제한을 둔다.
- 모델·tokenizer·MLX 버전을 고정하고 롤백 가능하게 배포한다.
- 메모리 압력, first-token latency, 생성 속도와 실패율을 운영 지표로 본다.
공식 자료
- MLX 저장소: https://github.com/ml-explore/mlx
- MLX 문서: https://ml-explore.github.io/mlx/build/html/
- MLX LM 저장소: https://github.com/ml-explore/mlx-lm
- MLX Swift 저장소: https://github.com/ml-explore/mlx-swift
이 글의 API와 요구 조건은 2026년 10월 23일 KST에 확인한 공식 자료를 기준으로 한다. MLX와 MLX LM은 빠르게 바뀌므로 실제 설치 전 해당 버전의 문서와 `--help`를 다시 확인한다.
MLX의 핵심은 단순히 "맥에서 GPU를 쓰는 라이브러리"가 아니다. 통합 메모리에 배열을 두고 CPU와 GPU가 같은 저장 공간을 사용하며, 지연 계산과 함수 변환으로 그래프를 구성하는 실행 모델이 중심이다. 이 구조를 이해하면 로컬 LLM의 메모리 사용, 학습 루프의 `mx.eval`, 컴파일과 캐시의 동작을 더 정확하게 다룰 수 있다.
댓글
0개아직 댓글이 없습니다.