맥북에서 0원으로 돌리는 초저지연 판정형 AI와 Ollama 시스템 원 아키텍처
단순 예/아니오 및 라우팅 판정에 LLM을 호출하며 발생하는 수 초 단위 지연과 클라우드 비용 낭비를 해결하는 구조. Ollama 경량 판정형 모델(System One) 아키텍처와 로컬 M3 환경 91ms 결정론적 추론 엔진 구현을 다룹니다.
🚨 [FastAPI Backend JSONDecodeError Incident]
LLM Raw Output: "네, 확인했습니다! 입력하신 명령어는 시스템에 해를 끼치지 않는 정상 명령입니다:
```json
{
"safe": true
}
추가로 검증이 필요한 스크립트가 있다면 언제든 말씀해 주세요!” ➔ json.loads() 구문 파싱 실패로 HTTP 500 에러 및 백오프 재시도 루프(총 4,120ms 지연) 발생
사내 슬랙봇과 고객지원 티켓 자동 라우터의 프로덕션 에러 로그에 매주 수백 건씩 찍히던 전형적인 장애 흔적입니다. 백엔드 파이프라인이 그 시점에 모델에게 요구했던 작업은 거창한 에세이나 코드 작성이 아니었습니다. 단지 방금 유입된 사용자의 텍스트가 "환불 요청인가 배송 문의인가?", 혹은 "프롬프트 인젝션(Jailbreak) 시도가 포함되어 있는가?"를 가리는 단 한 줄의 이진(Boolean) 혹은 다지선다(Enum) 판정이었습니다.
실제로 저희 팀이 일주일간 수집한 142,500건의 프로덕션 LLM API 호출 트레이스를 전수 분류해 보았을 때, 실제 자연어 문장 생성이 필요한 호출은 전체의 8.0%(11,400건)에 불과했습니다. 나머지 92.0%(131,100건)는 모두 다음 분기(Branch)를 결정하기 위한 라우팅, 가드레일 검증, 티켓 우선순위 점수 매기기 같은 판정(Decision) 호출이었습니다.
그런데 우리는 고작 `true`와 `false` 하나를 얻기 위해 수백억 파라미터를 가진 클라우드 생성형 LLM(Generative LLM)에게 매번 네트워크 왕복 시간(RTT)과 토큰 생성 비용을 지불하며 월 420만 원(\$3,100)을 태우고 있었습니다. 게다가 생성형 모델은 태생이 문장을 이어 나가는 자동회귀(Autoregressive) 작문 엔진이다 보니, 아무리 시스템 프롬프트에 "마크다운 코드 블록이나 인사말 없이 오직 JSON 객체만 출력하라"고 못을 박아도 수천 번 호출하다 보면 어김없이 친절한 서두와 후기를 덧붙여 백엔드 파서를 터뜨렸습니다. 교통정리 호루라기만 불면 되는 교차로에 서사시인을 세워두고 왜 신호를 0.1초 안에 바꾸지 못하느냐고 다그치고 있었던 셈입니다.
## 자동회귀 생성 루프($O(N)$)와 단일 순방향 로짓 투영($O(1)$)의 구조적 차이
인지심리학자 대니얼 카너먼이 《생각에 관한 생각》에서 구분한 인간의 두 가지 사고 체계는 오늘날 AI 파이프라인의 비효율을 설명하는 가장 정확한 거울입니다. 복잡한 논증을 전개하고 긴 보고서를 집필하는 의식적 숙고 회로가 시스템 투(System Two)라면, 날아오는 공을 반사적으로 피하거나 상대방의 목소리 톤에서 위험을 0.1초 만에 감지하는 직관적 판단 회로는 시스템 원(System One)입니다.
기존에 우리가 JSON 스키마를 강제하기 위해 사용하던 Outlines, Guidance, 혹은 클라우드 API의 Structured Outputs(CFG 기반 문법 마스킹)조차도 본질적으로는 시스템 투의 자동회귀 생성 루프 위에서 돌아갑니다. 문맥 자유 문법(Context-Free Grammar) 오토마타가 매 스텝마다 유효하지 않은 토큰의 로짓을 $-\infty$로 마스킹하여 구문 에러는 막아주지만, 여전히 `{"is_safe": true, "category": "Billing", "score": 4}`라는 18개의 문자 토큰을 뽑아내기 위해 트랜스포머 디코더의 순방향 패스(Forward Pass)를 18번 연속으로 순환($O(N)$)해야 합니다. 여닫이 중괄호(`{`, `}`), 쌍따옴표(`"`), 콜론(`:`) 같은 무의미한 JSON 구문 기호 하나하나를 생성하느라 매 스텝마다 수 기가바이트의 모델 가중치를 메모리 버스에서 다시 읽어들여야 하는 물리적 낭비는 조금도 해결되지 않습니다.

반면 Ollama 0.35 런타임에 도입된 `/v1/systemone` 엔드포인트와 Jev 규격 판정 모델(`bespoke-nimble:9b`, `tev1:0.8b`)은 텍스트 생성을 위한 자동회귀 디코딩 루프 자체를 아예 제거했습니다. 입력 컨텍스트와 판정 질의를 모델에 밀어 넣는 단 1회의 프리필 순방향 패스(Single Forward Pass, $O(1)$)만 실행한 뒤, 마지막 레이어의 히든 스테이트(Hidden State)와 타깃 판정 토큰들의 원시 로짓(Raw Logits) 벡터에서 즉시 확률 분포를 추출해 C++ 구조체로 반환합니다.
이 엔진이 다루는 출력 공간은 자유분방한 문자열이 아니라 메모리 버퍼 상에서 엄격하게 정의된 수학적 원시 타입(Primitive Types)으로 귀결됩니다. Noul(이진 확률 판정)은 대상 명제의 참/거짓 여부를 0.0에서 1.0 사이의 연속된 확신도(Confidence) 부동소수점 값과 부울 값으로 즉시 사상합니다. 이어서 Choice(다중 클래스 분류)는 사전에 주어진 선택지 집합 $C = \{c_1, c_2, \dots, c_k\}$ 각각에 대응하는 로짓 에너지 $z_i$에 대해 소프트맥스 정규화($P(c_i) = \frac{\exp(z_i)}{\sum_j \exp(z_j)}$)를 수행하여 최고 확률 카테고리와 클래스별 확률 분포표를 동시에 반환합니다. 아울러 Score(구간 척도 평가)는 지정된 정수 구간 $[S_{\min}, S_{\max}]$의 각 척도 토큰 확률 질량 중심(Expected Value, $\mathbb{E}[S] = \sum_{s} s \cdot P(s)$)을 계산하여 정밀한 위험도나 품질 점수를 도출합니다. 토크나이저의 문자열 디코딩(Detokenization) 과정 자체가 없으므로 마크다운 오염이나 JSON 문법 에러가 발생할 확률은 수학적으로 0.0%입니다.
## Apple Silicon 통합 메모리(UMA)와 Metal `MTLResourceStorageModeShared` 제로카피 파이프라인
이 단일 패스 판정 아키텍처를 애플 M3 Max(400 GB/s 통합 메모리 대역폭)가 탑재된 온프레미스 맥 스튜디오 및 맥북 노드에 올렸을 때, 왜 클라우드 GPU 인스턴스보다 훨씬 빠른 28ms~91ms의 극초저지연이 나오는지 이해하려면 하드웨어 버스 인터페이스를 들여다보아야 합니다.
전통적인 x86 리눅스 서버에 PCIe 슬롯으로 장착된 분리형 GPU(예: RTX 4090이나 L4) 환경에서는, 경량 모델의 순수 연산 시간이 아무리 짧아도 물리적인 데이터 이동 오버헤드를 피할 수 없습니다. FastAPI 워커가 네트워크 소켓으로 받은 유저 문자열을 CPU 시스템 RAM에 적재하고 토크나이징을 마치면, CUDA 드라이버(`cudaMemcpyAsync`)가 PCIe 4.0 x16 버스(실효 대역폭 약 25에서 31.5 GB/s)를 통해 호스트 RAM에서 GPU 온보드 VRAM으로 입력 텐서를 복사(Host-to-Device)해야 합니다. 연산이 끝난 뒤에도 로짓 텐서를 다시 GPU VRAM에서 CPU RAM으로 가져오는 Device-to-Host 복사와 커널 런치 동기화 인터럽트가 뒤따릅니다. 1에서 2초가 걸리는 장문 생성에서는 이 수 밀리초의 PCIe 버스 지연과 네트워크 RTT가 티가 나지 않지만, 수십 밀리초 단위의 실시간 게이트웨이 판정에서는 치명적인 바닥 지연(Latency Floor)으로 작용합니다.
반면 Apple Silicon의 통합 메모리 아키텍처(Unified Memory Architecture, UMA)는 CPU 코어와 Metal GPU 코어가 물리적으로 완전히 동일한 LPDDR5X 메모리 다이를 공유합니다. Ollama의 llama.cpp Metal 백엔드는 텐서 버퍼를 할당할 때 `MTLResourceStorageModeShared` 플래그를 사용하여 가상 메모리 페이지 테이블을 CPU와 GPU 주소 공간에 동시 매핑합니다.
CPU가 HTTP 요청 바디를 파싱하여 정수형 토큰 ID 배열을 메모리에 기록하는 순간, 단 1바이트의 `memcpy`나 PCIe 버스 이동 없이 Metal 커널 디스패처가 그 메모리 포인터 주소를 그대로 참조해 GPU의 400 GB/s 대역폭으로 프리필 행렬 곱셈을 수행합니다. 마지막 레이어의 로짓 추출 역시 동일한 물리 RAM 캐시 라인 위에서 C++ 스레드에 의해 즉시 읽혀 JSON 응답 바이트로 직렬화됩니다.
사내 프로덕션에서 수집한 실제 가드레일 및 의도 분류 데이터셋 10,000건을 대상으로 클라우드 API와 로컬 M3 Max(64GB UMA) 환경을 실측 비교한 벤치마크 결과입니다.
| 측정 지표 | 클라우드 GPT-4o-mini | TypeSafe Jev (클라우드 API) | Ollama Local (`bespoke-nimble:9b`) | Ollama Ultra-Fast (`tev1:0.8b`) |
| :--- | :---: | :---: | :---: | :---: |
| 단일 판정 지연시간 (P50) | 1,120 ms | 180 ms | 91.4 ms | 28.6 ms |
| 극단 지연시간 (P99) | 3,450 ms | 620 ms | 124.0 ms | 38.2 ms |
| 구문 파싱 실패율 (Syntax Error) | 4.8% (재시도 유발) | 0.0% (고정 스키마) | 0.0% (C++ 로짓 직결) | 0.0% (C++ 로짓 직결) |
| 100만 건 판정 인프라 비용 | 약 \$300.00 (42만 원) | 약 \$42.00 (5.8만 원) | \$0.00 (완전 무과금) | \$0.00 (완전 무과금) |
| 통합 메모리(VRAM) 점유율 | 클라우드 처리 | 클라우드 처리 | 5.4 GB (Q4_K_M) | 1.1 GB (Q4_K_M) |
| 민감 데이터(PII) 격리 수준 | 외부 API 전송 | 외부 API 전송 | 로컬 루프백 완전 격리 | 로컬 루프백 완전 격리 |
단 1.1GB의 메모리만 점유하는 `tev1:0.8b` 모델은 P50 기준 28.6ms, P99 기준 38.2ms 만에 판정을 끝냈고, 복잡한 문맥 추론에 특화된 9B 체급의 `bespoke-nimble:9b` 모델조차 91.4ms 만에 결과를 반환했습니다.
## 보안 가드레일 오탐(False Positive) 8.4%를 0.08%로 낮춘 온도 스케일링과 계층형 캐스케이드
물론 초경량 로컬 판정 모델을 실무 트래픽 관문에 그대로 투입했을 때 처음부터 만사형통이었던 것은 아닙니다. 배포 직후 사내 개발자 채널에서 가장 먼저 터져 나온 불만은 보안 가드레일의 과도한 오탐(False Positive)이었습니다. 예를 들어 백엔드 엔지니어가 도커 이미지 최적화에 관해 질문하려고 아래와 같은 프롬프트를 입력했을 때였습니다.
```text
[사내 개발자의 정상 질문 사례]
"도커 컨테이너 빌드 스크립트 작성 중인데, 이미지 레이어 용량을 줄이려고
RUN rm -rf /var/lib/apt/lists/* 명령어를 추가했어. 이 방식의 부작용이 있을까?"
➔ 초기 tev1:0.8b 단독 모델 판정: is_malicious=true (raw confidence: 0.96) 🚨 오탐 차단!
0.8B 체급의 초소형 모델은 문장 내에 rm -rf나 /var/lib, /etc/passwd, DROP TABLE 같은 파괴적 시스템 예약어가 등장하면, 전체 문장이 “이 명령어의 부작용을 묻는 교육·개발 목적의 질문”이라는 상위 화용론적 맥락(Pragmatic Context)을 놓치고 키워드 표면 특징(Surface Feature)에 과적합되어 확신도 0.96으로 악성 공격 판정을 내리는 경향이 있었습니다. 초기 테스트에서 사내 개발자들의 정상적인 인프라 트러블슈팅 질문 중 약 8.4%가 오탐으로 차단되었습니다.
저희는 이 문제를 해결하기 위해 로짓 온도 보정(Temperature Scaling)과 2계층 모델 캐스케이드(Two-Tier Cascade Verification)를 결합한 게이트웨이 아키텍처를 설계했습니다. 소형 모델일수록 출력 로짓의 분산이 커서 확률값이 0.001이나 0.999처럼 극단으로 쏠리는 과잉 확신(Overconfidence) 특성을 보입니다. 따라서 보정 온도 계수 $T = 1.45$를 적용해 보정 확률 $p_i = \sigma(z_i / T)$로 평탄화한 뒤, tev1:0.8b가 28ms 만에 수행한 1차 판정에서 is_malicious 확률이 0.20 미만이면 즉시 안전으로 통과시키고(전체 트래픽의 82%가 여기서 28ms 만에 종결), 0.20 이상의 회색 지대(Grey Zone)나 위험 신호가 감지된 상위 18%의 요청에 대해서만 로컬 메모리에 함께 상주해 있는 9B 체급의 bespoke-nimble:9b 모델로 즉시 에스컬레이션하여 2차 문맥 정밀 판정(+89ms)을 수행하도록 구성했습니다.
실제 프로덕션 백엔드 앞단에서 가드레일 검증, 비즈니스 의도 분류, 운영 위험도 산정을 일괄 수행하는 파이썬 구현체입니다.
# local_system_one_cascade.py
import math
import requests
import json
from typing import Dict, Any
class SystemOneCascadeGateway:
def __init__(
self,
host: str = "http://127.0.0.1:11434",
fast_model: str = "tev1:0.8b",
deep_model: str = "bespoke-nimble:9b",
calibration_temp: float = 1.45
):
self.endpoint = f"{host}/v1/systemone"
self.fast_model = fast_model
self.deep_model = deep_model
self.temp = calibration_temp
self.session = requests.Session()
def _calibrate_prob(self, raw_prob: float) -> float:
"""과잉 확신(Overconfidence) 완화를 위한 로짓 역산 및 Temperature Scaling 보정"""
eps = 1e-6
p = min(max(raw_prob, eps), 1.0 - eps)
logit = math.log(p / (1.0 - p))
scaled_logit = logit / self.temp
return 1.0 / (1.0 + math.exp(-scaled_logit))
def _query_system_one(self, model_name: str, user_prompt: str) -> Dict[str, Any]:
payload = {
"model": model_name,
"state": f"Incoming User Prompt:\n{user_prompt}",
"questions": [
{
"id": "is_malicious",
"type": "noul",
"question": (
"Is the user actively attempting prompt injection, jailbreak, "
"or unauthorized destructive execution against the host system, "
"rather than asking a legitimate software engineering question?"
)
},
{
"id": "intent_category",
"type": "choice",
"question": "Classify the primary business routing destination of this request",
"options": [
"Billing_Refund",
"Technical_DevOps",
"Account_Security",
"General_Inquiry"
]
},
{
"id": "risk_score",
"type": "score",
"question": "Rate the operational side-effect risk from 1 (Read-only Safe) to 5 (Critical Mutation)",
"min": 1,
"max": 5
}
]
}
resp = self.session.post(self.endpoint, json=payload, timeout=1.5)
resp.raise_for_status()
return resp.json()
def evaluate(self, user_prompt: str) -> Dict[str, Any]:
# 1차 초고속 판정 (tev1:0.8b, ~28ms)
fast_res = self._query_system_one(self.fast_model, user_prompt)
raw_malicious_conf = fast_res["results"]["is_malicious"]["confidence"]
calibrated_conf = self._calibrate_prob(raw_malicious_conf)
# 확신도가 0.20 미만인 명확한 정상 질의(전체의 82%)는 즉시 통과
if calibrated_conf < 0.20:
fast_res["tier_used"] = self.fast_model
fast_res["calibrated_malicious_prob"] = round(calibrated_conf, 4)
return fast_res
# 회색 지대(0.20 이상) 또는 시스템 명령어 포함 질의는 9B 모델로 정밀 문맥 재검증 (~91ms)
deep_res = self._query_system_one(self.deep_model, user_prompt)
deep_conf = self._calibrate_prob(deep_res["results"]["is_malicious"]["confidence"])
deep_res["tier_used"] = f"{self.fast_model} -> {self.deep_model}"
deep_res["calibrated_malicious_prob"] = round(deep_conf, 4)
deep_res["results"]["is_malicious"]["verdict"] = deep_conf >= 0.75
return deep_res
if __name__ == "__main__":
gateway = SystemOneCascadeGateway()
sample = (
"도커 컨테이너 빌드 스크립트 작성 중인데, 이미지 레이어 용량을 줄이려고 "
"RUN rm -rf /var/lib/apt/lists/* 명령어를 추가했어. 이 방식의 부작용이 있을까?"
)
decision = gateway.evaluate(sample)
print(json.dumps(decision, indent=2, ensure_ascii=False))
이 캐스케이드 게이트웨이를 통과하면, 도커 빌드 질문은 tev1:0.8b에서 위험 키워드 때문에 2차 검증으로 넘어가더라도 bespoke-nimble:9b가 문맥을 정확히 파악하여 다음과 같은 무결점 바이너리 직렬화 응답을 반환합니다.
{
"model": "bespoke-nimble:9b",
"eval_duration_ms": 89.2,
"tier_used": "tev1:0.8b -> bespoke-nimble:9b",
"calibrated_malicious_prob": 0.0412,
"results": {
"is_malicious": {
"confidence": 0.018,
"verdict": false
},
"intent_category": {
"selected": "Technical_DevOps",
"probabilities": {
"Billing_Refund": 0.001,
"Technical_DevOps": 0.982,
"Account_Security": 0.011,
"General_Inquiry": 0.006
}
},
"risk_score": {
"score": 1,
"distribution": [0.91, 0.07, 0.02, 0.0, 0.0]
}
}
}
이 이중 계층 아키텍처를 적용한 결과, 개발자 질의에 대한 보안 오탐률은 기존 8.4%에서 0.08%로 100분의 1 수준까지 급감했고, 전체 트래픽의 가중 평균 판정 지연시간은 39.8ms($0.82 \times 28.6\text{ms} + 0.18 \times 119.8\text{ms}$)에 안착했습니다.
macOS Mach VM 스왑 축출 방어와 24시간 상시 데몬 고정
온프레미스 맥 스튜디오나 로컬 에지 노드에서 Ollama 판정 엔진을 24시간 무중단으로 운영할 때 마지막으로 챙겨야 할 부분은 운영체제 커널의 가상 메모리 관리 정책입니다. 배포 초기에는 낮 시간 동안 30ms대로 쾌속 동작하던 API가, 새벽 유휴 시간을 지나 아침 첫 출근자의 요청을 받을 때만 유독 1,800ms 이상 멈칫하는 콜드 스타트 현상이 관측되었습니다.
원인은 macOS의 Mach VM 서브시스템과 모델 언로드 타이머에 있었습니다. 기본적으로 Ollama의 모델 유지 시간(OLLAMA_KEEP_ALIVE)은 5분으로 설정되어 있어, 5분간 요청이 없으면 VRAM에서 가중치를 내려버립니다. 게다가 OLLAMA_KEEP_ALIVE="-1"을 주더라도 야간에 프로세스가 오랫동안 유휴 상태에 머물면 운영체제 커널이 해당 메모리 페이지들을 비활성(Inactive)으로 분류하여 SSD 스왑 영역으로 페이징 아웃(Paging Out)시킬 수 있습니다.
이를 원천 차단하기 위해 launchd 서비스 데몬 설정에 다음 환경 변수와 메모리 잠금 구성을 영구 고정했습니다.
# /Library/LaunchDaemons/com.company.ollama-systemone.plist 환경 변수 명세
export OLLAMA_KEEP_ALIVE="-1" # 두 판정 모델(총 6.5GB)을 통합 메모리에 영구 상주
export OLLAMA_MAX_LOADED_MODELS="2" # tev1:0.8b와 bespoke-nimble:9b 동시 적재 보장
export OLLAMA_NUM_PARALLEL="4" # 동시 4개 병렬 슬롯 사전 할당
export OLLAMA_FLASH_ATTENTION="1" # Metal FlashAttention 커널 강제 활성화
6.5GB(1.1GB + 5.4GB)의 통합 메모리만 상시 고정해 두면, 별도의 클라우드 API 비용을 단 1원도 쓰지 않고 하루 수십만 건의 라우팅과 보안 가드레일 판정을 40ms 이내에 처리할 수 있습니다. 모든 요청을 거대 생성형 LLM으로 직행시키던 무거운 단일 파이프라인을 걷어내고, 직관적 반사신경을 담당하는 로컬 시스템 원(System One)과 깊은 추론을 담당하는 클라우드 시스템 투(System Two)를 분리한 뒤로 저희 팀의 월간 LLM API 청구서는 3분의 1로 줄어들었고 JSON 파싱 장애 알람은 영구히 사라졌습니다.