LoRA 어댑터
LoRA 어댑터 (LoRA Adapters)
모델 전체를 다시 학습하지 않고, 작은 어댑터만 교체해서 모델의 행동을 바꿀 수 있다면 얼마나 좋을까요? 그게 바로 LoRA(Low-Rank Adaptation) 예요. vLLM은 기본 모델(base model) 위에 다양한 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.generate를 lora_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_UPDATING을 True로 설정합니다.
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 serve가 200 OK 상태 코드로 응답하고, curl은 Success: 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_PLUGINS에lora_filesystem_resolver를 포함시키고VLLM_LORA_RESOLVER_CACHE_DIR을 로컬 디렉토리로 설정하세요. vLLM이foobar라는 LoRA 어댑터를 쓰는 요청을 받으면, 로컬 디렉토리에서foobar디렉토리를 찾아 그 내용을 LoRA 어댑터로 로드하려 시도해요. - Hugging Face Hub:
VLLM_PLUGINS에lora_hf_hub_resolver를 포함시키고VLLM_LORA_RESOLVER_HF_REPO_LIST를 Hub의 저장소 ID의 쉼표 구분 목록으로 설정하세요.my/repo/subpath요청이 오면my/repo의subpath에adapter_config.json이 있는지 확인하고 어댑터를 다운로드해요. 원격 다운로드는 안전하지 않으므로 운영 환경에 적합하지 않습니다.
직접 구현하기
자체 플러그인을 구현하는 예시 단계는 다음과 같아요.
- 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
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 모듈의 name과 path만 포함하고 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에서 래퍼를 선택하고, 어댑터는 그에 맞춰야 해요.