본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 24. 22:52

Sipp: 하이브리드 AI 애플리케이션을 위한 로컬 우선 (local-first) 런타임

요약

Sipp는 로컬 모델과 클라우드 모델을 통합하여 하이브리드 추론을 지원하는 로컬 우선(local-first) 런타임입니다. WebGPU를 활용해 브라우저 내에서 효율적인 추론을 가능하게 하며, 실시간 컨텍스트를 활용한 지능형 애플리케이션 구축을 돕습니다.

핵심 포인트

  • 로컬과 클라우드 모델을 결합한 하이브리드 추론 설계
  • WebGPU를 통한 브라우저 기반의 효율적인 모델 실행
  • 실시간 환경 컨텍스트를 활용한 개인화된 AI 경험 제공
  • 로컬 엔진의 요청 스케줄링 및 KV 캐시 상태 재사용

지난 몇 달 동안 저는 llama.cpp의 WebGPU 백엔드에 기여할 기회를 가졌으며, 이를 통해 고립된 연산자 (operator) 지원 단계에서 브라우저 기반 및 멀티모달 추론 (multimodal inference)을 위한 더욱 완전하고 신뢰할 수 있는 경로로 나아갈 수 있도록 도왔습니다. 이는 수십 명의 기여자가 함께한 공동의 노력이었으며, Sipp를 출시할 준비를 마치는 데 필수적인 구성 요소였습니다. 리드 메인테이너(lead maintainer)인 Reese Levine는 이에 대해 Llamas on the Web이라는 아주 멋진 블로그 포스트를 작성했으며, 아키텍처 설계에 관한 논문을 발표했습니다. 이 기술 포스트에서 저는 브라우저 내 WebGPU 추론에 대한 몇 가지 생각과, 지능을 더욱 쉽게 사용할 수 있도록 하기 위해 로컬 및 클라우드 컴퓨팅을 하이브리드 추론 (hybrid inference) 설계로 통합하려는 더 큰 생태계에 대해 공유하고자 합니다.

Sipp 확인하기: https://www.sipp.sh/

지능을 사용 가능하게 만든다는 것의 의미

기술은 실험실에서 작동할 때 흥미로워지며, 사람들이 실제로 자신들이 이미 사용하고 있는 환경에서 기술에 접근하고, 실행하고, 적응시키고, 이를 바탕으로 무언가를 구축할 수 있을 때 변혁적인 힘을 갖게 됩니다. Sipp가 AI를 더욱 사용 가능하게 만들고자 하는 것도 바로 그러한 의미입니다.

언어 모델(Language models)은 주로 채팅창을 통해 소프트웨어에 진입했지만, 대화형 애플리케이션이 완전한 프롬프트로 시작하는 경우는 드뭅니다. 게임, 디자인 도구, 에이전트 작업 공간(agent workspaces)에서는 열려 있는 문서, 선택된 객체, 최근 편집 사항, 사용자 작업, 장면 상태(scene state), 배경 작업과 같이 실시간 환경 자체가 유용한 컨텍스트(context)가 되는 경우가 많습니다. 오픈 모델(open models)이 더 작아지고 더 유능해짐에 따라, 이러한 작업의 더 많은 부분이 사용자 근처에서 수행될 수 있습니다. 이는 단순히 속도와 개인정보 보호 이상의 의미를 갖습니다. 유용한 컨텍스트가 모든 상호작용마다 클라우드 경계를 넘어야 하거나, 프런티어 모델(frontier-model)의 가격 책정에 의존하거나, 단일 제공업체에 의해 제한되어서는 안 되기 때문입니다. 프런티어 모델은 여전히 심층 추론(deep reasoning)과 계획(planning)에 더 뛰어나지만, 대부분의 대화형 소프트웨어는 모든 단계에서 그러한 깊이를 필요로 하지 않습니다. 더 강력한 설계 방식은 분리된 방식입니다. 즉, 즉각적이고 개인적이며 지속적인 상호작용을 위해서는 로컬 모델(local models)을 사용하고, 작업이 진정으로 더 깊은 추론을 필요로 할 때만 더 큰 원격 모델(remote models)을 사용하는 것입니다.

Sipp는 그 사이의 공간을 위해 설계되었습니다. 로컬 모델, 게이트웨이 대상(gateway target), 또는 제공업체 엔드포인트(provider endpoint)를 등록한 다음, 선택된 엔드포인트에 대해 동일한 작업을 호출할 수 있습니다. 남은 섹션에서는 다음과 같은 Sipp의 시스템 설계(system design)를 소개하겠습니다.

  • Sipp가 로컬, 게이트웨이, 제공업체 엔드포인트를 표현하는 방식
  • query, chat, embed가 별도의 작업인 이유
  • 로컬 엔진이 요청을 스케줄링하고 키-값(KV) 캐시 상태를 재사용하는 방식
  • 브라우저 호스트가 GGUF 모델에 대해 WebGPU를 실용적으로 만드는 방식
  • 게이트웨이가 원격 컴퓨팅에 대해 정책 및 작업 경계(policy and operations boundary)를 제공하는 방식

아키텍처 개요

Sipp는 엔드포인트 등록(endpoint registration)을 사용합니다. 엔드포인트는 동일한 프로세스 내, 브라우저 내, HTTP 게이트웨이 뒤, 또는 외부 제공업체에서 실행될 수 있습니다. 애플리케이션은 엔드포인트 참조를 유지하고 이를 query, chat, 또는 embed에 전달합니다.

flowchart LR
  App[Application] --> Client[SippClient]
  Client --> Local[Local endpoint]
...

이 다이어그램은 두 가지 중요한 속성을 보여줍니다. 첫째, 애플리케이션이 엔드포인트(endpoint)를 선택합니다. Sipp는 로컬 모델에서 원격 모델로 요청을 암묵적으로 이동시키지 않습니다. 애플리케이션이 개인정보 보호(privacy), 비용(cost), 지연 시간(latency), 품질(quality)과 같은 제품 제약 사항을 직접 관리합니다. 둘째, 운영 형태(operation shape)가 안정적으로 유지됩니다. 특정 기능은 브라우저 모델로 시작하여, 나중에 서버 측 로컬 모델을 추가하고, 더 어려운 요청을 위해 게이트웨이 타겟(gateway target)을 추가하더라도 공개된 운영 모델(public operation model)을 변경할 필요가 없습니다.

엔드포인트 모델 (Endpoint model)

핵심 클라이언트는 공통 추론 엔드포인트 인터페이스 뒤에 엔드포인트를 저장합니다. SippClient::add()는 디스크립터(descriptor)로부터 엔드포인트를 구축합니다:

  • 로컬 모델 디스크립터는 SippEngine을 로드합니다.
  • 게이트웨이(gateway) 디스크립터는 HTTP 게이트웨이 엔드포인트를 구축합니다.
  • 프로바이더(provider) 디스크립터는 프로바이더 지원이 활성화된 경우 직접적인 프로바이더 어댑터(provider adapter)를 구축합니다.

등록 후, Sipp는 운영(operations)을 선택된 엔드포인트로 분해(resolve)하고, 해당 엔드포인트가 요청된 운영을 지원하는지 확인하며, 동일한 실행(run) 및 응답(response) 타입을 반환합니다. 이러한 설계는 애플리케이션 API를 작게 유지하면서도 엔드포인트 선택을 명시적으로 유지합니다.

요청 운영 (Request operations)

Sipp는 query, chat, embed를 분리하는데, 이는 각 운영마다 서로 다른 런타임(runtime) 요구 사항을 갖기 때문입니다.

query는 채팅 템플릿(chat template)을 적용하지 않고 가공되지 않은 프롬프트(prompt) 문자열을 전송합니다. 애플리케이션이 완성형(completion-style) 프롬프트, 퓨샷(few-shot) 프롬프트, 커스텀 템플릿, 인코더-디코더(encoder-decoder) 흐름, 또는 프롬프트를 직접 생성하는 에이전트 루프(agent loops)와 같이 프롬프트 형식을 직접 제어하는 경우 이 운영을 사용합니다.

chat은 순서가 지정된 역할(role)과 콘텐츠(content) 메시지를 전송합니다. 로컬 엔드포인트는 모델의 채팅 템플릿을 적용합니다. 게이트웨이 또는 프로바이더 엔드포인트는 이러한 메시지를 선택된 원격 프로토콜(remote protocol)로 매핑합니다.

embed는 생성된 텍스트 대신 벡터(vectors)를 반환합니다. 엔드포인트는 임베딩(embeddings)을 지원해야 하며, 로컬 런타임은 출력 토큰을 샘플링하는 대신 임베딩 결과를 읽기 때문에 다른 경로를 사용합니다.

이러한 분리는 흔히 발생하는 통합 버그를 방지합니다. 가공되지 않은 프롬프트 (raw prompt)가 실수로 채팅 기록 (chat transcript)이 되지 않으며, 채팅 요청이 모델의 템플릿 (template) 없이 로컬 모델로 전송되지도 않습니다. 임베딩 요청 (embedding request)이 생성 전용 엔드포인트 (generation-only endpoint)로 라우팅되는 일도 없습니다.

게이트웨이 (gateway)와 프로바이더 (provider) 경로 또한 이러한 경계를 강제합니다. 게이트웨이 요청은 contextKey, grammar, 그리고 로컬 샘플링 오버라이드 (local sampling overrides)와 같은 로컬 전용 필드를 거부합니다. 엔드포인트별 옵션은 JSON 호환이어야 하며, model, prompt, messages, 또는 stream과 같은 타입이 지정된 필드를 오버라이드할 수 없습니다.

로컬 엔진 (Local engine)

로컬 엔진은 엔드포인트 호출을 대화형 추론 (interactive inference) 작업으로 전환하는 Sipp의 구성 요소입니다.

핵심 루프는 틱 (tick) 기반입니다. 요청이 런타임에 진입하면 내부 생성 (generation) 또는 임베딩 (embedding) 요청이 되며, 스케줄러 틱을 통해 진행됩니다. 이 모델은 눈에 보이는 채팅 응답, 짧은 백그라운드 분류 (background classification), 그리고 더 긴 프리필 (prefill)이 서로 중첩될 수 있는 대화형 애플리케이션에 적합합니다.

각 틱에서 엔진은 프롬프트 프리필 (prompt prefill)과 토큰 디코드 (token decode)를 분리합니다. 배치 플래너 (batch planner)는 준비된 슬롯 (slots)으로부터 토큰 기여분 (token contributions)의 평탄한 리스트 (flat list)를 구축합니다. 각 기여분은 Prefill 또는 Decode 중 하나이며, 슬롯 인덱스, 요청 ID, 토큰, 위치, 그리고 요청에 로짓 (logits)이 필요한지 여부를 포함합니다.

이러한 분리는 스케줄러가 지연 시간 (latency)과 처리량 (throughput)을 직접 제어할 수 있게 해줍니다:

  • 디코드 (Decode) 단계는 가시적인 토큰을 생성하기 때문에 지연 시간에 민감합니다.
  • 프리필 (Prefill) 단계는 첫 번째 생성된 토큰이 나오기 전에 많은 프롬프트 토큰을 처리할 수 있습니다.
  • 활성화된 디코드 스트림 (decode streams)이 긴 프롬프트 뒤에서 대기하지 않아야 합니다.
  • 디코드 스트림이 활성화되어 있는 동안에도 긴 프롬프트는 계속 진행되어야 합니다.

플래너는 디코드와 프리필의 예산을 별도로 할당합니다. 계획 (plan)을 재사용함으로써 스케줄러의 핫 패스 (hot path)에서 반복적인 할당을 피합니다. 또한 플래너는 매 틱마다 HashSet을 할당하는 대신, 점유된 슬롯을 계산하기 위해 작은 비트마스크 (bitmask) 패스트 패스 (fast path)를 사용합니다.

네이티브 백엔드 (native backend)가 실행된 후, Sipp는 요청 장부 기록 (request bookkeeping)을 적용하고 토큰 배치 (token batches)를 방출합니다. 런타임은 관찰 가능성 (observability)을 위해 요청 메트릭 (request metrics)을 기록합니다.

런타임 상태로서의 키-값 캐시 (Key-value cache)

각 로컬 요청은 contextKey를 가질 수 있습니다. 이 키는 문서, 장면 (scene), 대화, 워크스페이스 또는 백그라운드 작업과 같은 논리적 워크플로우 (workflow)를 나타냅니다. 엔진은 해당 키를 사용하여 라이브 KV 상태를 재사용할 수 있는지, 또는 프리픽스 스냅샷 (prefix snapshot)을 복구할 수 있는지를 결정합니다.

KvCacheManager는 컨텍스트 키 (context keys)를 물리적 시퀀스 슬롯 (physical sequence slots)에 매핑합니다. 요청이 완료되고 캐시 재사용이 활성화되면, Sipp는 해당 시퀀스를 유휴 (idle) 상태로 유지하면서도 메모리에 상주 (resident) 시킬 수 있습니다. 동일한 contextKey를 가진 나중의 요청은 해당 웜 상태 (warm state)를 사용할 수 있습니다. 만약 런타임에 물리적 시퀀스보다 더 많은 활성 컨텍스트 (active contexts)가 있다면, LRU 정책을 사용하여 유휴 세션을 제거 (evict) 합니다. Sipp는 프리픽스 스냅샷 (prefix snapshots)도 지원합니다. 프리필 (prefill) 단계 동안, 런타임은 동일한 모델 핑거프린트 (model fingerprint) 및 컨텍스트 범위 (context scope)에 대해 가장 잘 일치하는 스냅샷을 복구할 수 있습니다. 그런 다음 누락된 접미사 (suffix)만 다시 계산합니다. 프리필 경로 (prefill path)는 가장 긴 공통 접두사 (longest common prefix) 재사용을 계산하고, 해당 모델 제품군 (model family)에 대해 부분적 KV 재사용이 유효한지 확인하며, 컨텍스트 윈도우 (context window) 내에 공간을 확보하고, 캐시 히트 (cache hits)를 기록합니다.

이 캐시 상태는 하이브리드 라우팅 (hybrid routing)에서 중요한데, 이는 로컬 증거 (local evidence)를 요청하는 데 드는 비용을 나타내기 때문입니다. 따뜻한 (warm) contextKey가 있다면 런타임은 프리픽스 토큰 (prefix tokens)을 재사용하거나, 짧은 검증기 (verifier)를 실행하거나, 전체 프리필 (prefill) 비용을 다시 지불하지 않고도 답변 초안을 작성할 수 있습니다. 그러면 라우터는 로컬 결과를 반환할지, 초안을 감사를 위해 게이트웨이 (gateway)로 보낼지, 아니면 작업에 더 강력한 모델이 필요할 가능성이 높을 때 로컬 작업을 건너뛸지를 결정할 수 있습니다. 예를 들어, 에디터는 사용자가 하나의 파일 내에서 작업하는 동안 동일한 contextKey를 사용할 수 있습니다. 첫 번째 요청은 파일을 읽고 최근 편집 내용을 로컬 모델에 로드하는 비용을 지불합니다. "이 편집이 안전한가요?"와 같은 후속 요청은 해당 따뜻한 프리픽스를 재사용하여 저렴한 로컬 체크를 실행할 수 있습니다. 만약 체크 결과가 확신할 수 있다면 UI는 즉시 응답할 수 있습니다. 만약 체크 결과가 불확실하거나 더 많은 컨텍스트가 필요한 경우, 라우터는 대신 작업을 게이트웨이로 보낼 수 있습니다.

정적인 로컬-후-클라우드 (local-then-cloud) 계단식 구조 대신, 라우트는 캐시 히트 (cache hits), 프리필 비용 (prefill cost), 네트워크 지연 시간 (network latency), 그리고 제공자 비용 (provider cost)을 라우팅 입력값으로 사용할 수 있습니다. 저희는 현재 이 분야를 연구하고 있으며, 향후 이 주제에 대한 몇 가지 통찰을 제공할 수 있기를 기대합니다.

브라우저 호스트 (Browser host)

브라우저 호스트는 패키징, 모델 스테이징 (model staging), 기능 선택 (capability selection), 그리고 JavaScript 대상 런타임 API를 소유합니다. Rust 코어에 있는 추론 엔진 (inference engine)을 중복해서 생성하지는 않습니다.

빌드는 Rust 브라우저 ABI를 Emscripten 정적 라이브러리로 컴파일합니다. 그런 다음 해당 라이브러리를 llama.cpp, ggml, ggml-webgpu, 그리고 멀티모달 (multimodal) 런타임과 연결합니다. ggml-webgpu 타겟은 WGSL 셰이더 파일을 생성된 헤더에 내장하며, Emscripten 빌드는 Dawn의 emdawnwebgpu 포트를 사용하여 C++ 및 WebAssembly에서 브라우저 WebGPU를 호출합니다.

브라우저 클라이언트(browser client)는 워커 기반(worker-backed) 모델 서비스 또는 메인 스레드(main-thread) 모델 서비스를 통해 실행되며, 싱글 스레드(single-thread) 및 pthread WebAssembly 아티팩트(artifacts)를 전송합니다. pthread 아티팩트는 SharedArrayBuffer, 교차 출처 격리(cross-origin isolation), 그리고 공유 메모리를 활성화하는 배포 헤더(deployment headers)를 필요로 합니다. 이러한 요구 사항이 충족되지 않는 경우에도 싱글 스레드 아티팩트를 사용할 수 있습니다.

백엔드 선택은 기능 인식(capability-aware) 방식으로 이루어집니다:

  • 앱이 CPU를 요청하면, Sipp은 CPU를 사용합니다.
  • 앱이 WebGPU를 요청하면, Sipp은 어댑터(adapter) 정보를 반환합니다.
  • 앱이 자동 선택(automatic selection)을 사용하면, Sipp은 어댑터가 shader-f16을 노출할 때만 WebGPU를 선택하며, 그렇지 않으면 CPU로 폴백(fallback)합니다.

모델 로딩은 추론 성능(inference performance)의 일부입니다. 브라우저 캐시 정책은 최대 2 GiB까지 파일을 직접 로드합니다. 더 큰 GGUF 자산은 512 MiB 샤드(shards)로 분할하여 해당 샤드들을 자동으로 로드합니다. 대용량 자산의 경우, 브라우저 패키지는 파일을 OPFS에 저장하고, 동기 액세스 핸들(sync access handles)을 열어 해당 핸들을 Emscripten의 파일 시스템에 마운트(mount)합니다. 읽기 호출(Read calls)은 OPFS에서 WebAssembly 힙(heap)의 Uint8Array 뷰로 바이트를 직접 복사합니다. 이를 통해 JavaScript ArrayBuffer로 읽어들인 후 동일한 바이트를 HEAPU8로 다시 복사하는 과정을 피할 수 있습니다. 비전 모델(Vision models)도 동일한 라이프사이클(lifecycle)을 사용합니다. 메인 모델 가중치(weights)는 GGUF 샤드로 스테이징(staged)될 수 있으며, 프로젝터(projector) 아티팩트는 멀티모달 런타임(multimodal runtime)을 위해 별도로 스테이징됩니다.

WebGPU 사례 연구

WebGPU 백엔드가 ONNXTVM/WebLLM과 구별되는 주요 차이점은 표현 방식(representation)에 있습니다.

ONNX는 WebGPU를 ONNX 그래프를 위한 실행 제공자(execution provider)로 취급합니다. 이는 이식 가능한 그래프 아티팩트(graph artifacts)와 제공자 추상화(provider abstraction)에 적합합니다. 다만, 토크나이저 메타데이터(tokenizer metadata), 채팅 템플릿(chat templates), KV 동작(KV behavior), 그리고 llama.cpp 양자화 레이아웃(quantized layouts)과 같은 GGUF 네이티브 세부 사항들이 서로 다른 아티팩트 경계를 넘어야 한다는 트레이드오프(tradeoff)가 있습니다. TVM/WebLLM은 컴파일러 파이프라인(compiler pipeline)을 사용합니다. 모델 연산은 MLC-LLM과 Apache TVM을 통해 WebGPU 및 WebAssembly 아티팩트로 하향 변환(lowered)됩니다. 이 경로는 큐레이션된 모델 카탈로그(curated model catalog)를 위해 사전 최적화(ahead-of-time optimization), 그래프 퓨전(graph fusion), 연산자 스케줄링(operator scheduling)을 적용할 수 있습니다. 하지만 사용자가 임의의 GGUF 파일을 런타임에 지정하여 직접 실행할 수 없다는 트레이드오프가 존재합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0