실제 Local LLM 사용 후기를 읽는 것은 즐겁습니다. Local OCR 정확도를 높이기 위해 Andrej Karpathy’s Autosearch 적용해 본 이야기, 서로 다른 세대의 GPU 2대 역할을 나누어서 decode 속도를 향상 시킨 이야기를 소개합니다. 보너스로 2대 DGX Spark 와 DeepSeek 사용을 비교해 본 이야기도 흥미롭습니다.

Local OCR 파이프라인의 정확도를 60%에서 99%로 끌어올린 방법

AI 사용할 때, 단일 프롬프트 1회 문답으로 끝내는 것이 아니라 출력 결과를 자동 검증하고, 조정하는 과정을 반복(loop) 함으로써 최종 품질을 향상하는 핵심으로 보입니다. 글에서 언급한 Andrey Karpath’s Autoresearch 내용도 참고하면 좋겠습니다.

AD

AI 모델 종류에 따른 산출물 결과도 분명 있습니다만 이 글에서 언급하는 방식과 같이 사용자가 원하는 품질 기준을 명시하고, AI가 기준에 맞는 산출물이 나올 때까지 파라미터 조정하고, 시험을 반복하는 방식이 요즘 추세로 보입니다. 
====

목표와 제약

  • 수기 작성한 송장(Invoice)에서 신뢰 가능한 구조화된 데이터를 뽑아내는 로컬 OCR 파이프라인을 Mac Mini(Apple Silicon)에서 안정적으로 운영하는 것이 목표.
  • GPU/CUDA 의존 모델(예: DeepSeek-OCR)은 Apple Silicon에서 적합하지 않아 제외.

초기 상태

  • GLM-OCR(로컬, Ollama 통해 운용)을 사용해 시작했으며 초기 정확도는 약 60%로, 수기 항목과 포맷 오류가 잦았음.

개선 방법론 — “autoresearch” 루프 적용

  • Andrej Karpathy식 접근: 동일한 테스트셋과 채점 규칙을 고정, 한 번에 한 변수만 바꿔 자동으로 전체 테스트를 돌려 개선만 커밋하고 악화는 되돌림.
  • 이 루프로 프롬프트, 스키마(필드 순서), 출력 제한, 반복 민감도, 파서 처리 등을 체계적으로 실험.

프롬프트·파서 개선 결과

  • 반복 페널티(repeat_penalty)나 예시(few-shot) 변경이 일부 문제를 해결했으나 다른 케이스에선 역효과를 냄.
  • 몇 가지 조치로 정확도가 90%까지 상승:
    • 스키마 순서를 조정해 어려운 문서에서 라인 아이템을 회복.
    • 문제를 일으키던 few-shot 예시를 제거해 예시 복사 문제 해결.
    • 첫 번째 완전한 JSON 객체만 추출하고, 콤마 그룹 숫자 정규화 같은 후처리로 실패 케이스 복구.

이미지 전처리(비주얼 문제 해결)

  • 남은 오류는 희미한 파란색 펜 글씨 등 시각적 노이즈에서 발생.
  • 모델·프롬프트는 고정한 채 각기 다른 이미지 필터를 하나씩 테스트해서, 어려운 문서는 좋아지고 깔끔한 문서는 깨지지 않아야 한다는 조건을 적용.
  • 여러 필터 실험 결과: 단일 조합(레드 채널 추출 → 오토컨트라스트 → 가벼운 언샤프 마스크)이 문제 문서의 코드 복구는 물론, 깔끔한 컨트롤 문서에도 부작용을 일으키지 않아 채택됨.

최종 성과와 교훈

  • 최종 파이프라인: 이미지 → (combo filter: red-channel → autocontrast → light unsharp mask) → GLM-OCR → 파싱/정규화 → 인덱스
  • 내부 테스트셋에서 약 99% 정확도 달성(초기 60% → 90%(프롬프트+파서) → 99%(이미지 필터 추가)).
  • 핵심 교훈: 한 번에 하나씩 통제된 변화를 주고 동일한 입력으로 측정하는 반복적 실험(autoresearch)이 강력하다. 한 가지 ‘마법’ 솔루션보다 반복적 작은 개선의 누적이 중요.

MoE Expert block을 구형 RTX 1070에서 처리하여 decode 속도 81% 향상

TL;DR

Attention 레이어는 현대 텐서코어 카드(예: RTX 3050)에 두고, MoE의 expert block(메모리 중심)은 GTX 1070(또는 다른 Pascal 카드)으로 분산시켜서 CPU 오프로드를 줄이면 decode 속도가 크게(최대 81%) 개선된다. 다만 일부 모델/설정에서 prefill(프롬프트 준비)은 Pascal의 연산 한계로 느려질 수 있다.

상세 절차 (요약)

개념

  • 모델을 레이어 성격에 따라 분리: Attention = compute·텐서코어 필요, Experts = 주로 VRAM·대역폭 필요.
  • 목표: Experts를 시스템 RAM/CPU 대신 GTX 1070의 VRAM으로 올려 PCIe/VRAM 경로에서 처리하게 함.

환경(작성자 설정)

  • 운영체제: Linux(Windows에서는 혼합 세대 드라이버 문제로 잘 동작하지 않음).
  • 드라이버/툴체인: NVIDIA 580.xx 드라이버(작성자 기준), CUDA 12.8로 컴파일, GCC-14 사용, GSP 펌웨어 비활성화 필요할 수 있음.
  • 하드웨어 예: GPU0 = RTX 3050(6GB) → Attention, GPU1 = GTX 1070(8GB) → Experts, CPU 보조(예: Ryzen 3600 XT), 32GB RAM.
  • PCIe: 양 카드 모두 PCIe3 x8로 연결(작성자 테스트).

실행(주요 옵션)

  • llama.cpp 등 실행 시 아키텍처 인지 라우팅 옵션 사용(작성자는 -ot 와 -ts 같은 플래그 사용).
  • GGUF 헤더를 읽어 자동으로 레이어 분할/스플릿 계산을 해주는 관리 툴을 사용하면 수동 계산 부담을 줄일 수 있음(작성자는 Pascal’s Power GUI 제작).
아키텍처 인지 라우팅 ‘아키텍처 인지 라우팅(architecture-aware routing)’ 옵션(-ot, -ts 플래그 사용)은 MoE (Mixture of Experts) 모델의 expert 블록을 서로 다른 GPU 아키텍처에 맞게 효율적으로 분산 처리하기 위한 기능입니다.

  • -ot (architecture-aware offload toggle): 아키텍처 인지 오프로드 기능을 활성화하여 expert 블록을 여러 GPU에 분산할지 여부를 결정.
  • -ts (thread scheduling or tuning setting): GPU별 스레드 스케줄링이나 실행 튜닝 옵션을 지정해 각 아키텍처에 맞는 최적화된 실행을 지원.

아키텍처 인지 라우팅의 주요 동작 과정

  1. GPU 아키텍처 및 자원 인식: GPU들의 세대, 메모리 크기, 텐서코어 지원 여부, PCIe 대역폭 등을 파악
  2. Expert 블록 분할 및 할당: 모델 내 MoE expert 블록을 GPU별로 분할합니다.
  3. 실행 경로 및 스케줄링 최적화: 각 GPU의 특성에 맞는 커널과 연산 방식을 선택합니다.
  4. 동적 라우팅 및 프로파일링: 실행 중에 각 GPU의 부하와 처리 속도를 모니터링하여 필요 시 expert 블록 할당을 조정할 수 있음. 프로파일링 데이터를 바탕으로 최적의 분산 전략을 지속적으로 개선.

세부 설정 팁

  • 컨텍스트/캐시: 테스트는 64k 컨텍스트, Q8 KV 캐시로 측정.
  • 벤치 명령에서 prefill/배치/ubatch를 크게 잡아(-p 2048 -b 2048 -ub 2048) 하드웨어의 피크 잠재력 측정(작성자 방식). 실제 사용 시 작은 배치로 조정하면 VRAM 여유가 생겨 레이어 배치가 달라질 수 있음.
  • 혼합 세대 카드 간 P2P 미지원 시 내부적으로 시스템 RAM을 경유하는 ‘double hop’이 발생함(퍼포먼스 영향은 상황에 따라 상이).

벤치마크 요약 (작성자 표 요약)

  • 기준: “With 1070” vs “Without 1070” (prefill t/s / decode t/s), 변화량 = Prefill Change, Decode Change.
  • GPT-OSS (20B, Q4_K_M, 모델크기 10.8GB)
    • With 1070: prefill 939.51 t/s / decode 49.73 t/s
    • Without 1070: prefill 1127.46 t/s / decode 27.44 t/s
    • Prefill: -16.7% (느려짐)
    • Decode: +81.2% (대폭 향상)
  • Gemma 4 (26B, IQ4_XS, 12.6GB)
    • With 1070: prefill 695.81 / decode 23.15
    • Without 1070: prefill 744.32 / decode 14.57
    • Prefill: -6.5%
    • Decode: +58.9%
  • Gemma 4 (26B, Q4_K_M, 15.9GB)
    • With 1070: prefill 658.64 / decode 30.15
    • Without 1070: prefill 637.30 / decode 24.35
    • Prefill: +3.4%
    • Decode: +23.8%
  • Qwen3.6 (35B, Q4_K_M, 21.1GB)
    • With 1070: prefill 592.95 / decode 31.12
    • Without 1070: prefill 512.43 / decode 28.45
    • Prefill: +15.7%
    • Decode: +9.4%

핵심 관찰 포인트

  • Decode 향상은 GTX 1070이 맡은 expert 비중에 비례: 더 많은 expert를 1070이 수용할수록 decode 속도 상승 폭이 큼(GPT-OSS에서 최대).
  • Prefill(프롬프트 처리)은 상황에 따라 느려질 수 있음. 이유: Pascal 세대(1070)는 텐서코어가 없어 대형 배치(-ub 크게)에서 GEMM 연산을 비효율적으로 수행해 개별 커널 시간이 길어질 수 있음. 반면 대형 배치는 호출 수가 적어 전체 토큰 처리량이 더 좋은 경우도 있음(케이스 바이 케이스).
  • Ampere ↔ Pascal 간 직접 P2P 미지원이면 데이터가 시스템 RAM을 경유(PCIe double hop). 트레이스상 대량의 cudaMemcpyPeerAsync와 수십 GB 전송이 발생했지만, 작성자는 decode 단계에서 전송이 병목이 되지는 않았다고 보고함.
  • 드라이버·컴파일·설정 복잡성 존재: 혼합 세대/구형 카드 조합은 Linux에서 추가 설정(특정 드라이버 버전, 펌웨어 비활성화 등)이 필요.

(보너스) DeepSeek V4 Flash API가 이렇게 싼데, 왜 DGX Spark 2대를 돌릴까 ?


 

※ 출처: r/LocalLLM, r/openclaw, r/unsloth, r/opencode, r/claude

AD

LEAVE A REPLY

Please enter your comment!
Please enter your name here