프로젝트 소개
AirLLM은 대규모 언어 모델 추론을 위한 Python 도구로, 핵심 아이디어는 모델 전체를 한 번에 VRAM에 올리는 것이 아니라 모델을 더 세분화된 레이어 단위로 나누어 추론 시 필요에 따라 로드하는 것입니다. 이렇게 하면 매우 낮은 VRAM 환경에서도 70B, 235B, 671B, 심지어 Kimi K3 같은 더 거대한 MoE 대형 모델도 시도해 볼 수 있습니다.
Kimi K3가 최근 많은 주목을 받는 이유도 분명합니다. 초대규모 MoE 모델에 속하며 파라미터 규모는 매우 크지만, 각 token마다 실제로는 일부 expert에만 라우팅되기 때문입니다. AirLLM은 바로 이 점을 활용해 expert 단위의 스트리밍 로딩으로 전체 모델을 한 번에 VRAM에 올리지 않도록 합니다.
이번 글에서는 최소 프로젝트를 함께 구성해 봅니다. 먼저 Kimi K3 추론을 실행해 보고, 이후 이를 간단한 HTTP API로 감싸 보겠습니다. 마지막으로는 언제 로컬 저VRAM 추론이 적합한지, 그리고 언제는 Defapi 같은 호환 인터페이스를 직접 쓰는 것이 더 편한지도 함께 살펴보겠습니다.
난이도: 중급 | 소요 시간: 30-60분 | 학습 성과: AirLLM의 저VRAM 추론 흐름을 이해하고, Kimi K3 최소 추론 서비스를 실행해 보기
대상 독자
- 단일 GPU, 저VRAM 머신에서 초대형 모델을 체험해 보고 싶은 개발자
- Kimi K3, DeepSeek-V3, Qwen3-235B 같은 모델의 추론 비용을 평가하려는 엔지니어
- AI 애플리케이션 프로토타입을 만들고 있으며, 먼저 로컬에서 모델 효과를 검증하고 싶은 사람
- 로컬 모델을 API로 감싸 Agent, RAG 또는 업무 시스템에 연동하려는 개발자
핵심 의존성 및 환경
Kimi K3는 환경 조건이 꽤 까다롭기 때문에, 먼저 의존성 경계를 명확히 하겠습니다:
| 의존성 | 권장 버전 | 설명 |
|---|---|---|
| Python | 3.10+ | 가상환경으로 의존성 분리를 권장 |
| NVIDIA Driver | CUDA 12 지원 | CUDA 12 버전 PyTorch를 정상 실행할 수 있어야 함 |
| PyTorch | CUDA 12 빌드 | Kimi K3 의존성 체인은 CUDA 12에 더 적합 |
| transformers | 4.56.x | Kimi K3의 remote model code는 버전 요구사항이 있음 |
| airllm | 최신 버전 | AutoModel로 모델을 통합 로드 |
| compressed-tensors | 최신 버전 | Kimi K3 관련 weight 포맷에서 사용 |
| flash-attn | CUDA 12 호환 | Kimi K3 모델 코드는 보통 flash attention을 요구 |
WARNING
AirLLM은 VRAM 사용량을 줄여주지만, 모델 파일 자체를 작게 만들지는 않습니다. 최초 실행 시 모델을 다운로드하고 분할하게 되므로, 디스크 공간과 네트워크 품질 역시 매우 중요합니다. Kimi K3처럼 용량이 큰 모델은 충분한 모델 캐시 공간을 미리 확보하는 것이 좋습니다.
전체 프로젝트 구조
먼저 가장 작은 프로젝트를 준비해 보겠습니다. 구조는 단순하게 유지합니다:
airllm-kimi-k3-demo/
├── .env.example
├── requirements.txt
├── run_kimi_k3.py
├── server.py
└── README.md
1. Python 가상환경 생성
먼저 디렉터리와 가상환경을 생성합니다:
mkdir airllm-kimi-k3-demo
cd airllm-kimi-k3-demo
python -m venv .venv
source .venv/bin/activate
Windows PowerShell에서는 다음처럼 활성화할 수 있습니다:
mkdir airllm-kimi-k3-demo
cd airllm-kimi-k3-demo
python -m venv .venv
.venv\Scripts\Activate.ps1
Python 버전을 확인합니다:
python --version
최소 Python 3.10.x가 출력되는 것을 권장합니다. 머신에 여러 Python 버전이 있다면 py -3.10 -m venv .venv로 명시적으로 지정할 수 있습니다.
2. AirLLM 및 Kimi K3 의존성 설치
새로 requirements.txt를 생성합니다:
airllm
accelerate
sentencepiece
safetensors
compressed-tensors
python-dotenv
fastapi
uvicorn[standard]
먼저 PyTorch를 설치합니다. CUDA 12.1 예시는 다음과 같습니다:
pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
그다음 핵심 의존성을 설치합니다:
pip install -r requirements.txt
pip install "transformers>=4.56,<4.57"
마지막으로 flash-attn을 설치합니다:
pip install flash-attn --no-build-isolation
TIP
flash-attn 컴파일이 오래 걸린다면, 먼저 현재 환경에서 import torch가 정상 동작하는지와 torch.version.cuda가 CUDA 12 계열을 가리키는지 확인하세요. Kimi K3 같은 모델은 환경 의존성이 강하므로, 혼란스러운 전역 Python 환경에 바로 설치하는 것은 권장하지 않습니다.
3. 모델 권한 및 캐시 디렉터리 설정
일부 Hugging Face 모델은 로그인 또는 권한 승인이 필요합니다. 먼저 .env.example을 준비합니다:
HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
HF_HOME=/data/huggingface
AIRLLM_MODEL_ID=moonshotai/Kimi-K3
실제로 사용할 때는 복사본을 만듭니다:
cp .env.example .env
Windows PowerShell:
Copy-Item .env.example .env
그다음 자신의 Hugging Face Token을 입력합니다. 모델 ID는 공식적으로 실제 공개된 저장소 이름을 기준으로 해야 하며, 미러 저장소나 내부 동기화 저장소를 사용한다면 해당 repo id로 바꿔도 됩니다.
WARNING
.env 파일을 Git 저장소에 커밋하지 마세요. 모델 Token은 민감 정보이므로, 유출될 경우 private model 권한이 악용될 수 있습니다.
4. 최소 Kimi K3 추론 스크립트 작성
새로 run_kimi_k3.py를 생성합니다:
import os
from dotenv import load_dotenv
from airllm import AutoModel
def main() -> None:
"""Kimi K3를 로드하고 최소 텍스트 생성을 한 번 실행한다."""
load_dotenv()
model_id = os.getenv("AIRLLM_MODEL_ID", "moonshotai/Kimi-K3")
hf_token = os.getenv("HF_TOKEN")
# 입력 길이를 제어해, 먼저 짧은 prompt로 연결을 검증하고 최초 실행 시간을 줄인다.
max_length = 256
# AutoModel은 모델 유형을 자동으로 인식한다. hf_token은 권한이 필요한 모델 저장소 접근에 사용된다.
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
profiling_mode=True,
)
prompt = "AirLLM이 왜 대형 모델 추론의 VRAM을 낮출 수 있는지 세 문장으로 설명해줘."
# tokenizer는 여전히 Hugging Face 스타일을 사용하므로 이전 비용이 낮다.
input_tokens = model.tokenizer(
[prompt],
return_tensors="pt",
return_attention_mask=False,
truncation=True,
max_length=max_length,
padding=False,
)
# AirLLM 추론 시 weight를 레이어 단위로 로드한다. max_new_tokens는 먼저 작게 두어 빠르게 검증한다.
generation_output = model.generate(
input_tokens["input_ids"].cuda(),
max_new_tokens=128,
use_cache=True,
return_dict_in_generate=True,
)
output = model.tokenizer.decode(
generation_output.sequences[0],
skip_special_tokens=True,
)
print(output)
if __name__ == "__main__":
main()
실행:
python run_kimi_k3.py
최초 실행은 보통 느립니다. AirLLM이 모델을 다운로드하고, weight 파일을 분석한 뒤, 이후 레이어 단위로 로드할 수 있는 형식으로 모델을 분할해야 하기 때문입니다. 두 번째 실행부터는 모델 캐시와 분할 결과가 준비되어 있어 훨씬 더 원활해집니다.
5. VRAM 및 디스크 사용량 관찰
다른 터미널을 열어 VRAM을 확인합니다:
nvidia-smi -l 1
다음 두 가지 지표를 주의 깊게 봅니다:
Memory-Usage
GPU-Util
정상이라면 전통적인 transformers.from_pretrained()처럼 모델 전체가 즉시 올라와 VRAM을 폭발적으로 채우는 일은 없습니다. AirLLM의 방식은 “가면서 꺼내 쓰는” 방식에 가깝습니다. 어떤 레이어나 어떤 expert가 필요하면 해당 weight를 불러오고, 사용이 끝나면 다시 해제합니다.
디스크 사용량도 확인합니다:
du -sh "$HF_HOME"
Windows PowerShell에서는 다음을 사용할 수 있습니다:
Get-ChildItem $env:HF_HOME -Recurse | Measure-Object Length -Sum
TIP
저VRAM 추론은 공짜가 아닙니다. AirLLM은 부담을 VRAM에서 디스크 IO, CPU 메모리, 모델 캐시로 옮기므로, SSD 속도가 체감 성능에 직접적인 영향을 줍니다.
6. 압축 파라미터 활성화
AirLLM은 compression 파라미터를 통해 weight 로딩 크기를 줄일 수 있습니다. 모델 초기화 시 4bit를 추가해 볼 수 있습니다:
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
compression="4bit", # 4bit weight compression을 사용해 로딩 크기를 줄이고 일부 경우 속도를 개선
profiling_mode=True,
)
더 보수적으로 시작하고 싶다면 8bit부터 시도할 수 있습니다:
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
compression="8bit", # 8bit는 더 보수적이며 속도와 정밀도 사이의 균형이 좋음
profiling_mode=True,
)
보통은 다음처럼 선택합니다:
| 시나리오 | 권장 |
|---|---|
| Kimi K3가 실행되는지만 확인 | 압축 없이 먼저 경로 확인 |
| 디스크 IO가 명확히 느림 | 8bit 시도 |
| 속도와 저비용 검증을 더 중시 | 4bit 시도 |
| 진지한 평가를 진행 | 파라미터를 고정하고 여러 차례 출력 품질 비교 |
WARNING
압축은 weight 로딩 방식을 바꾸므로, 많은 추론 작업에는 영향이 작더라도 모델 평가, 장문 추론, 민감한 업무 검증을 한다면 반드시 비압축 버전을 기준선으로 보존해야 합니다.
7. 간단한 HTTP API로 감싸기
로컬 스크립트가 동작하면 보통 애플리케이션에 연결하고 싶어집니다. 아래는 FastAPI를 이용한 최소 서비스입니다.
새로 server.py를 생성합니다:
import os
from typing import List, Literal
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
from airllm import AutoModel
class ChatMessage(BaseModel):
"""OpenAI 스타일의 메시지 구조로, 나중에 호환 API로 바꾸기 쉽도록 한다."""
role: Literal["system", "user", "assistant"]
content: str
class ChatRequest(BaseModel):
"""최소 채팅 요청으로, 튜토리얼에 필요한 필드만 유지한다."""
messages: List[ChatMessage]
max_tokens: int = 256
class ChatResponse(BaseModel):
"""텍스트 결과를 반환하며, 우선은 단순하게 유지해 디버깅을 쉽게 한다."""
text: str
load_dotenv()
app = FastAPI(title="AirLLM Kimi K3 Demo")
model_id = os.getenv("AIRLLM_MODEL_ID", "moonshotai/Kimi-K3")
hf_token = os.getenv("HF_TOKEN")
# 서비스 시작 시 모델을 로드한다. 실제 운영 환경에서는 health check와 lazy loading을 추가할 수 있다.
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
profiling_mode=False,
)
def build_prompt(messages: List[ChatMessage]) -> str:
"""여러 turn의 메시지를 간단한 prompt로 연결한다. 나중에 모델 공식 chat template으로 교체 가능하다."""
lines: list[str] = []
for message in messages:
lines.append(f"{message.role}: {message.content}")
lines.append("assistant:")
return "\n".join(lines)
@app.post("/chat", response_model=ChatResponse)
def chat(request: ChatRequest) -> ChatResponse:
"""Kimi K3 텍스트 생성을 한 번 실행한다."""
prompt = build_prompt(request.messages)
input_tokens = model.tokenizer(
[prompt],
return_tensors="pt",
return_attention_mask=False,
truncation=True,
max_length=1024,
padding=False,
)
generation_output = model.generate(
input_tokens["input_ids"].cuda(),
max_new_tokens=request.max_tokens,
use_cache=True,
return_dict_in_generate=True,
)
text = model.tokenizer.decode(
generation_output.sequences[0],
skip_special_tokens=True,
)
return ChatResponse(text=text)
서비스 시작:
uvicorn server:app --host 0.0.0.0 --port 8000
호출 테스트:
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "Kimi K3 같은 MoE 모델이 expert 단위 스트리밍 로딩에 적합한 이유를 한국어로 설명해줘."
}
],
"max_tokens": 200
}'
이제 최소 API가 준비되었습니다. 아직 production-grade는 아니지만, 로컬 실험, Agent 프로토타입, Prompt 디버깅, 모델 효과 비교에는 충분합니다.
8. 로컬 추론과 Defapi 인터페이스의 선택 기준
AirLLM의 가치는 분명합니다. 저VRAM 머신으로 초대형 모델을 다룰 수 있게 해 주며, 특히 학습, 검증, 평가, 오프라인 실험에 매우 적합합니다. 하지만 운영 환경에서는 처리량, 지연 시간, 동시성, 모델 업데이트, 장애 대응, 운영 비용도 함께 고려해야 합니다.
단순히 모델 기능을 빠르게 애플리케이션에 연결하고 싶거나, OpenAI/Anthropic/Gemini 스타일의 통합 API가 필요하다면 Defapi가 더 가볍습니다. 장점은 가격이 보통 공식 대비 절반 수준이며, 일반적인 모델들이 다음 프로토콜과 호환된다는 점입니다:
v1/chat/completionsv1/messagesv1beta/models/
예를 들어 위의 로컬 /chat 호출을 하나의 통합 클라이언트로 추상화할 수 있습니다. 개발 단계에서는 로컬 AirLLM을 사용하고, 운영 환경에서는 Defapi로 전환하는 방식입니다:
import os
import requests
def ask_model(prompt: str) -> str:
"""환경 변수에 따라 로컬 추론 또는 Defapi 호환 인터페이스를 선택한다."""
provider = os.getenv("MODEL_PROVIDER", "local")
if provider == "defapi":
response = requests.post(
"https://api.defapi.org/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {os.environ['DEFAPI_API_KEY']}",
"Content-Type": "application/json",
},
json={
"model": os.getenv("DEFAPI_MODEL", "anthropic/claude-sonnet-4.5"),
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.7,
},
timeout=60,
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
response = requests.post(
"http://localhost:8000/chat",
json={
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 256,
},
timeout=300,
)
response.raise_for_status()
return response.json()["text"]
이 방식의 장점은 경계가 매우 명확하다는 점입니다. 로컬 모델은 탐색과 검증을 맡고, Defapi는 안정적이고 통일된 방식으로 저비용 운영 연동을 담당합니다. OpenClaw, RAG 서비스, 자동화 Agent에도 이런 이중 모드가 매우 유용합니다.
자주 발생하는 문제 해결
1. flash-attn 설치 실패 시 어떻게 하나요?
먼저 PyTorch가 CUDA를 정상 인식하는지 확인합니다:
python -c "import torch; print(torch.__version__); print(torch.version.cuda); print(torch.cuda.is_available())"
만약 torch.cuda.is_available()가 False라면, flash-attn부터 설치하려 하지 말고 PyTorch, 드라이버, CUDA 버전부터 수정해야 합니다. Kimi K3 같은 모델은 CUDA 12 빌드를 권장하며, CUDA 13에서는 적절한 사전 빌드 패키지가 없어 컴파일에 실패할 수 있습니다.
2. transformers 버전 오류는 어떻게 해결하나요?
버전을 4.56.x로 고정합니다:
pip uninstall -y transformers
pip install "transformers>=4.56,<4.57"
그다음 다시 실행합니다:
python run_kimi_k3.py
모델 remote code 로딩이 실패한다면 대개 transformers 버전과 모델 코드가 맞지 않는 경우입니다.
3. Hugging Face 다운로드가 실패하면 어떻게 하나요?
먼저 Token이 유효한지 확인합니다:
huggingface-cli whoami
로그인하지 않았다면:
huggingface-cli login
.env에 있는 HF_TOKEN만 사용해도 됩니다. 모델 접근 권한 신청이 필요한 경우에는 모델 페이지에서 먼저 약관에 동의해야 합니다.
4. 디스크 공간이 부족하면 어떻게 하나요?
AirLLM의 최초 실행은 원본 weight를 다운로드하고, 분할된 모델 파일도 생성합니다. 캐시 디렉터리를 더 큰 디스크로 옮길 수 있습니다:
export HF_HOME=/mnt/models/huggingface
또는 .env에서 설정합니다:
HF_HOME=/mnt/models/huggingface
그래도 공간이 부족하다면 AirLLM의 delete_original=True를 사용해 변환 후 원본 weight를 삭제할 수 있습니다:
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
delete_original=True, # 변환 완료 후 원본 weight를 삭제하여 디스크 공간 절약
)
5. 최초 실행이 왜 이렇게 느린가요?
정상입니다. 최초 실행에는 weight 다운로드, 인덱스 읽기, 모델 분할, 캐시 기록 등이 포함됩니다. 두 번째 실행부터는 캐시를 재사용하므로 훨씬 빨라집니다.
프로파일링을 활성화할 수도 있습니다:
model = AutoModel.from_pretrained(
model_id,
hf_token=hf_token,
profiling_mode=True, # 로딩 및 추론 시간을 출력해 병목을 찾기 쉽게 함
)
시간의 대부분이 디스크 읽기에 쓰인다면, 더 큰 VRAM으로 바꾸는 것보다 더 빠른 NVMe SSD를 사용하는 편이 더 효과적일 수 있습니다.
6. VRAM이 예상만큼 줄지 않으면 어떻게 하나요?
코드에서 전통적인 로딩 방식을 혼용하지 않았는지 먼저 확인합니다:
from airllm import AutoModel
model = AutoModel.from_pretrained(model_id)
전통적인 로딩 방식과 혼용하지 마세요:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(model_id)
후자는 전통적인 로딩 경로를 타므로 VRAM을 쉽게 가득 채울 수 있습니다.
7. API 서비스 동시성이 높아지면 왜 멈추나요?
로컬 저VRAM 추론은 순차적인 실험에 더 적합하며, 고동시성 처리를 바로 맡기기에는 적합하지 않습니다. 가장 간단한 방법은 우선 동시성을 제한하는 것입니다:
uvicorn server:app --host 0.0.0.0 --port 8000 --workers 1
더 안정적인 운영 호출이 필요하다면, 운영 요청은 Defapi로 전환하고 로컬 AirLLM은 평가와 디버깅 용도로 유지하는 것이 좋습니다.
추가 읽을거리 / 심화 방향
- AirLLM에서 Kimi K3, DeepSeek-V3, Qwen3-235B의 로딩 시간, VRAM 사용량, 출력 품질을 비교하기
- FastAPI 서비스를 OpenAI 호환
/v1/chat/completions인터페이스로 바꾸기 - Defapi를 연동해
v1/chat/completions또는v1/messages로 운영 모델 호출을 통합 관리하기 - AirLLM 서비스에 queue를 추가해 여러 요청이 동시에 디스크 IO와 GPU를 경쟁하지 않도록 하기
- OpenClaw 또는 다른 Agent 프레임워크에서 로컬 모델과 클라우드 모델을 함께 구성하고, 작업 복잡도에 따라 동적으로 선택하기