콘텐츠로 이동

LoRA 어댑터

mlxcel의 구현 범위

LoRA는 두 low-rank matrix로 weight update를 표현합니다.

W_fused = W_base + scale × (B @ A)

mlxcel은 adapter directory에서 대응하는 .lora_a와 .lora_b tensor를 읽어 두 가지 방식 중 하나로 적용합니다.

  • Fused. 모델 생성 전에 delta를 base weight map에 더합니다. mlxcel generate --adapter는 항상 fuse하고, 서버는 --lora-fuse를 지정했을 때와 pipeline parallelism에서 fuse합니다. Fused adapter는 decode에 추가 연산을 만들지 않으며, scale을 load 이후에 바꿀 수 없습니다.
  • Unfused runtime. mlxcel serve와 mlxcel-server는 adapter마다 live scale을 가진 low-rank term을 유지합니다. Scale은 startup에서 정하거나(--lora는 1.0, --lora-scaled는 지정값, --lora-init-without-apply는 0.0으로 시작), POST /lora-adapters로 바꾸거나, request마다 선택합니다. --adapter / --lora를 주면 서버의 기본 방식이 이 경로입니다.

여러 adapter를 unfused 또는 --lora-fuse로 fuse한 상태에서 동시에 load할 수 있으며 각 term이 합산됩니다.

디렉터리 계약

Adapter directory에는 다음 파일이 필요합니다.

adapter_config.json
adapters.safetensors          # 우선 사용

adapters.safetensors가 없으면 단일 fallback 이름 adapter_model.safetensors를 시도합니다. Sharded adapter file이나 임의의 여러 .safetensors file은 자동 탐색하지 않습니다.

Loader가 인식하는 tensor key suffix는 정확히 다음 두 가지뿐이며, 그 밖의 tensor(DoRA magnitude vector, 떠도는 .weight 등)가 파일에 있으면 거부합니다.

<base-weight-stem>.lora_a
<base-weight-stem>.lora_b

한 pair에 두 matrix가 모두 있어야 합니다. Stem을 대응 base weight에 resolve하고 최종 delta shape가 정확히 같아야 하며, 위험한 transpose/broadcast 가능 mismatch는 거부합니다. 차원이 명확하면 mlx-lm orientation (in, rank) / (rank, out)과 일반적인 matrix orientation (rank, in) / (out, rank)을 모두 허용합니다.

이 계약은 HuggingFace PEFT 전체 호환을 뜻하지 않습니다. adapter_model.safetensors라는 파일명은 허용하지만 .lora_A.weight 같은 PEFT key, sharded index, 다른 형태의 configuration은 mlxcel key/config 계약으로 변환해야 합니다.

설정

adapter_config.json은 mlxcel/mlx-lm 스타일 nested parameter를 사용합니다.

{
  "fine_tune_type": "lora",
  "lora_parameters": {
    "rank": 16,
    "lora_alpha": 32,
    "dropout": 0.0
  },
  "num_layers": -1,
  "target_modules": ["q_proj", "k_proj", "v_proj", "o_proj"],
  "model": "mistralai/Mistral-7B-v0.3"
}
Field 기본값 Runtime 의미
fine_tune_type lora lora 지원; dora와 full은 거부
lora_parameters.rank 8 Validation/log 및 alpha scaling에 사용
lora_parameters.scale 20.0 lora_alpha가 없을 때의 명시적 fusion scale
lora_parameters.lora_alpha 미설정 있으면 effective scale은 lora_alpha / rank
lora_parameters.dropout 0.0 Parse하지만 inference에서는 무시
num_layers, target_modules, model 다양 Parse되는 metadata이며 실제 fuse target은 .lora_a/.lora_b tensor key로 결정

fine_tune_type: "dora"는 load 시점에 DoRA adapters are not supported 오류로 거부됩니다. Low-rank pair만 적용하면 base model과도 fine-tune과도 맞지 않는 weight가 되기 때문입니다. DoRA adapter는 해당 도구로 base checkpoint에 먼저 fuse하십시오.

그 밖의 거부 사례는 모두 weight를 바꾸기 전 load 단계에서 발생합니다: 대응하는 base weight가 없는 pair, 3-D pair(mixture-of-experts expert adapter, mlx-lm LoRASwitchLinear), PEFT .lora_A.weight / .lora_B.weight 이름, 어떤 tensor도 적용하지 못하는 adapter, quantized base weight에 대한 fusion(오류가 quantized base에서도 adapter를 서빙하는 unfused --lora 경로를 안내합니다).

Embedding과 출력 head pair

mlx-lm LoRAEmbedding layout(lora_a [vocab, rank], lora_b [rank, hidden])의 embed_tokens pair는 입력 lookup에 적용되며, 출력 head가 embedding과 tied이면 head에도 적용됩니다. 같은 projection의 linear 형태 orientation 두 가지도 vocabulary 크기와 대조해 허용합니다. Fused(generate의 --adapter, --lora-fuse)와 unfused(--lora) load 모두 지원하지만, tie_word_embeddings: true인 Llama decoder(llama, mistral, qwen2)와 config에서 tie_word_embeddings: false로 두지 않은 Qwen3에 한합니다. 그 밖의 계열에서 tied checkpoint의 embed_tokens pair는 거부되며, tied checkpoint의 lm_head pair는 모든 load 경로에서 거부됩니다.

CLI

mlxcel generate \
  --model ./models/Mistral-7B-v0.3 \
  --adapter ./adapters/mistral-sql-lora \
  --prompt "Write a SQL query for orders over 1000 dollars." \
  --max-tokens 200

mlxcel generate --adapter는 load 시점에 adapter를 fuse합니다. 지원하지 않는 model family는 adapter를 조용히 무시하지 않고 명시적으로 실패합니다.

서버

mlxcel serve \
  --model ./models/Mistral-7B-v0.3 \
  --adapter ./adapters/mistral-sql-lora \
  --port 8080

서버에서 --adapter와 --lora는 같은 flag입니다. 쉼표로 구분한 adapter directory 목록을 scale 1.0으로 받습니다. --lora-scaled FNAME:SCALE,...은 scale을 명시한 adapter를 추가합니다(마지막 콜론에서 나누며, 숫자가 아니거나 유한하지 않은 scale은 startup 오류입니다). --lora-init-without-apply는 나열한 모든 adapter를 scale 0.0으로 load합니다. --lora-fuse는 adapter를 unfused로 유지하지 않고 weight에 합칩니다. flag 표는 CLI 레퍼런스를 참조하십시오.

항목 Unfused runtime (기본) --lora-fuse
GET /lora-adapters id, path, scale, task_name, prompt_prefix를 나열하고 live server scale을 보고합니다. load 시점에 고정된 scale을 나열합니다.
POST /lora-adapters [{"id": 0, "scale": 0.5}] 형식을 받으며 나열하지 않은 adapter는 0.0이 됩니다. 이후에 admit된 request에 적용되고, 진행 중인 generation은 admit 당시 snapshot을 유지합니다. 이미 적용된 설정과 같은 request만 허용합니다.
/completion request의 lora field 같은 [{id, scale}] 형식이며 해당 request에만 적용됩니다. 이미 적용된 설정과 같을 때만 허용합니다.

Adapter는 term을 합산하는 방식으로 쌓이므로 순서가 결과를 바꾸지 않습니다. Unfused 경로에서는 prompt-cache key에 유효 adapter scale의 digest가 들어가므로 다른 scale에서 계산된 cached prefix는 재사용되지 않습니다.

Tensor parallelism은 LoRA를 지원하지 않으며 runtime이 startup에서 거부합니다. Pipeline parallelism은 scale을 지정하지 않은 단일 adapter를 fuse 방식으로만 허용하고, tensor 또는 pipeline parallelism과 함께 쓴 --lora-scaled나 여러 개의 --lora는 startup 오류입니다. In-process pipeline parallelism은 각 stage에 맞는 adapter tensor만 filter합니다. Remote pipeline startup과 Muse Glimmer는 adapter를 거부합니다. VLM과 special model의 지원 여부는 loader마다 다르므로 명시적인 startup error를 따르고 사용할 checkpoint를 직접 검증하십시오.

성능과 메모리

Fused adapter는 생성 토큰마다 별도 adapter matrix multiplication을 수행할 필요를 없애므로 decode loop에 LoRA branch가 없습니다. Unfused adapter는 runtime scale 제어와 quantized base 지원을 얻는 대신 매 forward pass에 low-rank product를 더합니다. Fusion이 모든 측면의 비용을 '0'으로 만드는 것은 아닙니다.

  • Startup은 adapter pair마다 matrix product와 add를 수행합니다.
  • 일반 non-pipeline 경로는 fusion 전에 base weight map을 clone하므로 load peak memory가 늘 수 있습니다. Stage-local pipeline 경로는 stage map에 in-place로 fuse합니다.
  • 결과 fused tensor도 메모리를 차지하며 quantized/packed base layout을 adapter에 맞추기 위해 dequantize/requantize하지 않으며, 그런 weight에 fuse하는 것은 거부됩니다. Fused weight는 base dtype을 유지합니다.
  • 실제 throughput은 dtype, weight layout, backend, fusion 이후 load representation에 따라 달라질 수 있습니다.

배포 전에 짧은 output 비교와 실제 workload의 memory/throughput test를 수행하십시오.