LoRA 어댑터

LoRA 어댑터 (LoRA Adapters)

모델 전체를 다시 학습하지 않고, 작은 어댑터만 교체해서 모델의 행동을 바꿀 수 있다면 얼마나 좋을까요? 그게 바로 LoRA(Low-Rank Adaptation) 예요. vLLM은 기본 모델(base model) 위에 다양한 LoRA 어댑터를 올려서, 요청별로 효율적으로 서빙할 수 있는 기능을 제공합니다.

출처: vLLM 공식 문서 — lora

LoRA 어댑터란?

LoRA 어댑터는 기본 모델 위에 얹어 쓰는 작은 가중치 집합이에요. 전체 모델을 다시 학습 대신, 이 어댑터만 교체하면 되죠. LoRA 어댑터는 SupportsLoRA를 구현하는 모든 vLLM 모델과 함께 사용할 수 있어요.

오프라인에서 LoRA 사용하기

어댑터는 요청별(per-request)로 최소한의 오버헤드로 효율적으로 서빙할 수 있어요. 먼저 어댑터를 다운로드해서 로컬에 저장합니다.

from huggingface_hub import snapshot_download

sql_lora_path = snapshot_download(repo_id="jeeejeee/llama32-3b-text2sql-spider")

그다음 기본 모델을 인스턴스화하고 enable_lora=True 플래그를 넘깁니다.

from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest

llm = LLM(model="meta-llama/Llama-3.2-3B-Instruct", enable_lora=True)

이제 프롬프트를 제출하고 llm.generatelora_request 파라미터와 함께 호출하면 돼요. LoRARequest의 첫 번째 파라미터는 사람이 알아볼 수 있는 이름, 두 번째는 어댑터의 전역 고유 ID, 세 번째는 LoRA 어댑터의 경로예요.

sampling_params = SamplingParams(
    temperature=0,
    max_tokens=256,
    stop=["[/assistant]"],
)

prompts = [
    "[user] Write a SQL query to answer the question based on the table schema.\n\n context: CREATE TABLE table_name_74 (icao VARCHAR, airport VARCHAR)\n\n question: Name the ICAO for lilongwe international airport [/user] [assistant]",
    "[user] Write a SQL query to answer the question based on the table schema.\n\n context: CREATE TABLE table_name_11 (nationality VARCHAR, elector VARCHAR)\n\n question: When Anchero Pantaleone was the elector what is under nationality? [/user] [assistant]",
]

outputs = llm.generate(
    prompts,
    sampling_params,
    lora_request=LoRARequest("sql_adapter", 1, sql_lora_path),
)

비동기 엔진과 더 고급 설정 옵션을 사용하는 예시는 examples/features/lora/multilora_offline.py에서 확인할 수 있어요.

LoRA 어댑터 서빙하기 (Serving LoRA Adapters)

LoRA 적용 모델은 OpenAI 호환 vLLM 서버로도 서빙할 수 있어요. 서버를 띄울 때 --lora-modules {name}={path} {name}={path}로 각 LoRA 모듈을 지정하면 됩니다.

vllm serve meta-llama/Llama-3.2-3B-Instruct \
    --enable-lora \
    --lora-modules sql-lora=jeeejeee/llama32-3b-text2sql-spider

서버 엔트리포인트는 다른 LoRA 설정 파라미터(max_loras, max_lora_rank, max_cpu_loras 등)도 모두 받으며, 이후 모든 요청에 적용됩니다. /models 엔드포인트에 조회하면 LoRA와 기본 모델을 함께 볼 수 있어요.

curl localhost:8000/v1/models | jq .
{
    "object": "list",
    "data": [
        {
            "id": "meta-llama/Llama-3.2-3B-Instruct",
            "object": "model",
            ...
        },
        {
            "id": "sql-lora",
            "object": "model",
            ...
        }
    ]
}

요청은 마치 다른 모델인 것처럼 model 요청 파라미터로 LoRA 어댑터를 지정할 수 있어요. 요청은 서버 전역의 LoRA 설정에 따라 처리됩니다 (기본 모델 요청과 병렬로, 그리고 max_loras가 충분히 높다면 다른 LoRA 어댑터 요청과도 병렬로요).

curl http://localhost:8000/v1/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "sql-lora",
        "prompt": "San Francisco is a",
        "max_tokens": 7,
        "temperature": 0
    }' | jq

LoRA 어댑터 동적 서빙 (Dynamically Serving LoRA Adapters)

서버 시작 시점뿐 아니라, 런타임 중에 전용 API 엔드포인트와 플러그인을 통해 LoRA 어댑터를 동적으로 구성할 수도 있어요. 모델을 즉석에서 바꿔야 할 때 특히 유용하죠.

⚠️ 경고: 이 기능에는 보안 위험이 있어요. 격리된 완전 신뢰 환경이 아니라면 운영 환경에서 사용하지 마세요.

동적 LoRA 구성을 켜려면 환경 변수 VLLM_ALLOW_RUNTIME_LORA_UPDATINGTrue로 설정합니다.

export VLLM_ALLOW_RUNTIME_LORA_UPDATING=True

API 엔드포인트 사용하기 (Using API Endpoints)

LoRA 어댑터 로드: /v1/load_lora_adapter 엔드포인트에 POST 요청을 보내 LoRA 어댑터를 동적으로 로드할 수 있어요. 요청 페이로드에는 어댑터의 이름과 경로가 포함됩니다.

curl -X POST http://localhost:8000/v1/load_lora_adapter \
-H "Content-Type: application/json" \
-d '{
    "lora_name": "sql_adapter",
    "lora_path": "/path/to/sql-lora-adapter"
}'

성공하면 vllm serve200 OK 상태 코드로 응답하고, curlSuccess: LoRA adapter 'sql_adapter' added successfully 본문을 반환해요. 어댑터를 찾을 수 없거나 로드할 수 없는 등 오류가 발생하면 적절한 오류 메시지가 반환됩니다.

LoRA 어댑터 언로드: 이전에 로드한 LoRA 어댑터를 내리려면 /v1/unload_lora_adapter 엔드포인트에 어댑터의 이름이나 ID를 담아 POST 요청을 보내면 돼요.

curl -X POST http://localhost:8000/v1/unload_lora_adapter \
-H "Content-Type: application/json" \
-d '{
    "lora_name": "sql_adapter"
}'

성공하면 Success: LoRA adapter 'sql_adapter' removed successfully 본문이 반환됩니다.

플러그인 사용하기 (Using Plugins)

또는 LoRAResolver 플러그인으로 LoRA 어댑터를 동적으로 로드할 수 있어요. LoRAResolver 플러그인은 로컬 파일시스템, S3 같은 로컬·원격 소스에서 LoRA 어댑터를 로드할 수 있게 해줍니다. 매 요청마다 아직 로드되지 않은 새 모델 이름이 나타나면, LoRAResolver가 해당 LoRA 어댑터를 해석하고 로드하려 시도해요.

서로 다른 소스에서 LoRA 어댑터를 로드하고 싶다면 LoRAResolver 플러그인을 여러 개 설정할 수 있어요. 예를 들어 로컬 파일용 리졸버 하나, S3용 리졸버 하나를 둘 수 있죠. vLLM은 가장 먼저 찾은 LoRA 어댑터를 로드합니다.

기존 플러그인을 설치하거나 직접 구현할 수 있어요. 기본적으로 vLLM은 로컬 디렉토리에서 LoRA 어댑터를 로드하는 리졸버 플러그인과 Hugging Face Hub의 저장소에서 로드하는 리졸버 플러그인을 함께 제공합니다. 이 리졸버들을 쓰려면 VLLM_ALLOW_RUNTIME_LORA_UPDATING을 True로 설정해야 해요.

  • 로컬 디렉토리: VLLM_PLUGINSlora_filesystem_resolver를 포함시키고 VLLM_LORA_RESOLVER_CACHE_DIR을 로컬 디렉토리로 설정하세요. vLLM이 foobar라는 LoRA 어댑터를 쓰는 요청을 받으면, 로컬 디렉토리에서 foobar 디렉토리를 찾아 그 내용을 LoRA 어댑터로 로드하려 시도해요.
  • Hugging Face Hub: VLLM_PLUGINSlora_hf_hub_resolver를 포함시키고 VLLM_LORA_RESOLVER_HF_REPO_LIST를 Hub의 저장소 ID의 쉼표 구분 목록으로 설정하세요. my/repo/subpath 요청이 오면 my/reposubpathadapter_config.json이 있는지 확인하고 어댑터를 다운로드해요. 원격 다운로드는 안전하지 않으므로 운영 환경에 적합하지 않습니다.

직접 구현하기

자체 플러그인을 구현하는 예시 단계는 다음과 같아요.

  1. LoRAResolver 인터페이스 구현
import os
import s3fs
from vllm.lora.request import LoRARequest
from vllm.lora.resolver import LoRAResolver

class S3LoRAResolver(LoRAResolver):
       def __init__(self):
           self.s3 = s3fs.S3FileSystem()
           self.s3_path_format = os.getenv("S3_PATH_TEMPLATE")
           self.local_path_format = os.getenv("LOCAL_PATH_TEMPLATE")

       async def resolve_lora(self, base_model_name, lora_name):
           s3_path = self.s3_path_format.format(base_model_name=base_model_name, lora_name=lora_name)
           local_path = self.local_path_format.format(base_model_name=base_model_name, lora_name=lora_name)

           # Download the LoRA from S3 to the local path
           await self.s3._get(
               s3_path, local_path, recursive=True, maxdepth=1
           )

           lora_request = LoRARequest(
               lora_name=lora_name,
               lora_path=local_path,
               lora_int_id=abs(hash(lora_name)),
           )
           return lora_request
  1. LoRAResolver 플러그인 등록
from vllm.lora.resolver import LoRAResolverRegistry

s3_resolver = S3LoRAResolver()
LoRAResolverRegistry.register_resolver("s3_resolver", s3_resolver)

자세한 내용은 vLLM의 플러그인 시스템을 참고하세요.

제자리 LoRA 재로딩 (In-Place LoRA Reloading)

동적으로 LoRA 어댑터를 로드할 때, 이름은 유지한 채 기존 어댑터를 업데이트된 가중치로 바꿔야 할 수도 있어요. load_inplace 파라미터가 이 기능을 활성화합니다. 이는 비동기 강화학습 설정에서 어댑터가 계속 업데이트되고 진행 중인 추론을 방해하지 않고 교체되는 상황에서 흔히 발생해요.

load_inplace=True로 설정하면 vLLM은 기존 어댑터를 새 것으로 교체합니다.

curl -X POST http://localhost:8000/v1/load_lora_adapter \
-H "Content-Type: application/json" \
-d '{
    "lora_name": "my-adapter",
    "lora_path": "/path/to/adapter/v2",
    "load_inplace": true
}'

--lora-modules의 새 형식 (New format for --lora-modules)

이전 버전에서는 LoRA 모듈을 키-값 쌍 또는 JSON 형식으로 제공했어요.

--lora-modules  sql-lora=jeeejeee/llama32-3b-text2sql-spider

이 방식은 각 LoRA 모듈의 namepath만 포함하고 base_model_name을 지정할 방법이 없었죠. 이제 JSON 형식으로 name과 path 옆에 base_model_name도 지정할 수 있어요.

--lora-modules '{"name": "sql-lora", "path": "jeeejeee/llama32-3b-text2sql-spider", "base_model_name": "meta-llama/Llama-3.2-3B-Instruct"}'

하위 호환성을 위해 기존 키-값 형식(name=path)도 여전히 사용할 수 있지만, 그 경우 base_model_name은 미지정으로 남습니다.

2D와 3D MoE LoRA 어댑터 혼합 (Mixing 2D and 3D MoE LoRA Adapters)

같은 엔진 인스턴스에서 2D 형식(megatron 기반)과 3D 형식(peft 기반) 어댑터를 함께 서빙하려면 --enable-mixed-moe-lora-format으로 서버를 시작하고 각 어댑터의 레이아웃을 is_3d_lora_weight 필드로 명시하면 돼요.

vllm serve Qwen/Qwen3.6-35B-A3B \
    --enable-lora \
    --enable-mixed-moe-lora-format \
    --tensor-parallel-size 4 \
    --enable-expert-parallel \
    --lora-modules \
        '{"name": "lora-2d", "path": "jeeejeee/qwen36-35ba3b-2d-weights-poken-lora", "is_3d_lora_weight": false}' \
        '{"name": "lora-3d", "path": "jeeejeee/qwen36-35ba3b-moe-all-linear-poken-lora", "is_3d_lora_weight": true}'

/v1/load_lora_adapter로 동적 로드도 가능해요.

curl -X POST http://localhost:8000/v1/load_lora_adapter \
-H "Content-Type: application/json" \
-d '{
    "lora_name": "lora-3d",
    "lora_path": "/path/to/3d-format-lora",
    "is_3d_lora_weight": true
}'

⚠️ 어댑터의 레이아웃을 반드시 알아야 해요! --enable-mixed-moe-lora-format 아래에서 vLLM은 호출자가 선언하는 is_3d_lora_weight를 그대로 믿습니다 — 체크포인트를 검사해 확인하지 않아요. 잘못된 선언은 가중치를 잘못된 스택 버퍼에 로드해서 로드 시점에 오류 없이 조용히 잘못된 출력을 만들 수 있어요. 서빙 전에 레이아웃을 확인하세요.

  • 2D (per-expert, megatron 방식)is_3d_lora_weight: false. 어댑터 키가 ...experts.{idx}.gate_proj.lora_A.weight처럼 expert마다 한 세트씩 있어요.
  • 3D (fused, peft 방식)is_3d_lora_weight: true. 어댑터 키가 ...experts.gate_up_proj.lora_A.weight처럼 선행 차원에 모든 expert를 스택한 단일 텐서예요.

--enable-mixed-moe-lora-format이 설정되지 않으면 is_3d_lora_weight는 무시됩니다. vLLM은 기본 모델의 is_3d_moe_weight에서 래퍼를 선택하고, 어댑터는 그에 맞춰야 해요.

더 알아보기 (Learn more)