본문 바로가기
Python/패키지 훓어보기

[LLaMA-Factory] LoRA Adapter 확인

by beeny-ds 2025. 2. 27.
LLM 오픈소스 중 가장 유명한 LLaMA-Factory 를 파악하고 있다.
그 중 학습 시 LoRA 를 활용한다면 어떤 Adapter 를 생성하여 학습하는지 확인했다.

본 포스팅은 오픈소스인 LLaMA-Factory 에서 LoRA 활용한 학습 시 Adapter 및 arguments 셋팅이 어떻게 되어 있는지 코드를 통해 확인한 결과를 소개한다.

목차

1. 실무자는 바쁘다.! 결론부터 말씀드릴게요.

2. 생성되는 LoRA Adapter 확인 및 커스터마이즈 

3. LoRA Config 설정 for Hyper-Parameter 셋팅

4. 필자 리뷰

 

출처: LLaMA-Factory github


1. 실무자는 바쁘다.! 결론부터 말씀드릴게요.

필자가 파악하고자 한 주요 원인은 다음과 같다.

LLaMA-Factory 로 내가 원하는 모델 구조를 학습해주려면 어느 정도 커스터마이징 해야할까?

 

LLaMA-Factory github 의 README.md 를 보면 예시가 대부분 LoRA 를 활용한 PT 또는 SFT 학습이다.

필자가 궁금했던 것 중 하나는 LoRA 를 활용할 때 어떤 Layer 에 Adapter 를 추가하는지이다.

예제에 보면 lora_target = all 로 되어 있지만 all 의 의미가 구체적으로 무엇인지 직접 확인해보았다.

  • nn.linear layer 에 대해서만 Adapter 를 붙여준다.
  • nn.Embedding layer 에 대해서는 Adapter 를 붙여주지 않는다.

즉, LLaMA-Factory 에서는 LoRA Adapter 를 nn.Embedding layer 에도 추가하고 싶다면 코드를 일부 수정해줘야 한다.

 

그렇다면 구체적으로 LoRA Adapter 가 왜 nn.linear 에만 추가되는지, 어떻게 nn.Embedding layer 에 추가해줄 수 있는지 함께 확인해보자.


2. 생성되는 LoRA Adapter 확인 및 커스터마이즈   

LLaMA-Factory 에서 학습 config 중  lora_target = all 와 같이 설정하면 llama model 은 아래와 같이 구성된다. (더보기를 클릭해보자.)

더보기

PeftModelForCausalLM(
  (base_model): LoraModel(
    (model): LlamaForCausalLM(
      (model): LlamaModel(
        (embed_tokens): Embedding(128256, 2048)
        (layers): ModuleList(
          (0-15): 16 x LlamaDecoderLayer(
            (self_attn): LlamaSdpaAttention(
              (q_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (k_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=512, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=512, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (v_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=512, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=512, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (o_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (rotary_emb): LlamaRotaryEmbedding()
            )
            (mlp): LlamaMLP(
              (gate_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=8192, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=8192, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (up_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=8192, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=8192, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (down_proj): lora.Linear(
                (base_layer): Linear(in_features=8192, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=8192, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (act_fn): SiLU()
            )
            (input_layernorm): LlamaRMSNorm((2048,), eps=1e-05)
            (post_attention_layernorm): LlamaRMSNorm((2048,), eps=1e-05)
          )
        )
        (norm): LlamaRMSNorm((2048,), eps=1e-05)
        (rotary_emb): LlamaRotaryEmbedding()
      )
      (lm_head): Linear(in_features=2048, out_features=128256, bias=False)
    )
  )
)

 

더보기의 내용을 하나하나 살펴보면 알겠지만(그게 귀찮긴 하다) nn.Embedding layer 에 대해서는 lora_adapter 가 생성되지 않는다. 이는 곧 nn.Embedding layer 인 Embedding 과 LM-HEAD layer 에 대해서는 weight 를 freeze 해주고 학습하지는 않는다는 걸 의미한다.

 

그래서 필자 해당 부분은 어디서 이렇게 설정해주는건지 확인해봤다.

  • LLaMA-Factory/src/llamafactory/model/adapter.py 에서 학습할 lora adapter 를 리스트업한다.
    • target_modules = find_all_linear_modules(model, finetuning_args.freeze_vision_tower) 를 참고하면 된다.
  • find_all_linear_modules 의 함수는 LLaMA-Factory/src/llamafactory/model/model_utils/misc.py 에 있다.
    • forbidden_modules = {"lm_head"} 를 통해 lm_head layer 는 lora adapter 를 생성하지 않음을 알 수 있다.
    • if "Linear" in module.__class__.__name__ and "Embedding" not in module.__class__.__name__: 를 통해 nn.linear layer 만 lora adapter 를 생성함을 알 수 있다.

 

그렇다면 embedding, lm_head layer 도 학습해주려면 어떻게 해줘야할까?

필자가 이것저것 테스트해보니 가장 쉬운 방법은 LLaMA-Factory/src/llamafactory/model/adapter.py 를 수정하는 거다.

이렇게 수정해주자.

    if is_trainable and adapter_to_resume is None:  # create new lora weights while training
        if len(finetuning_args.lora_target) == 1 and finetuning_args.lora_target[0] == "all":
            target_modules = find_all_linear_modules(model, finetuning_args.freeze_vision_tower)
            target_modules += ["embed_tokens", "lm_head"]    # target 모듈에 embedding, lm_head layer 직접 추가
        else:
            target_modules = finetuning_args.lora_target

위와 같이 셋팅하면 아래와 같이 embedding, lm_head layer 에 대해서도 lora adapter 가 생성됨을 확인할 수 있다. (더보기를 클릭해보자.)

더보기

PeftModelForCausalLM(
  (base_model): LoraModel(
    (model): LlamaForCausalLM(
      (model): LlamaModel(
        (embed_tokens): lora.Embedding(
          (base_layer): Embedding(128256, 2048)
          (lora_dropout): ModuleDict(
            (default): Identity()
          )
          (lora_A): ModuleDict()
          (lora_B): ModuleDict()
          (lora_embedding_A): ParameterDict(  (default): Parameter containing: [torch.cuda.FloatTensor of size 8x128256 (cuda:0)])
          (lora_embedding_B): ParameterDict(  (default): Parameter containing: [torch.cuda.FloatTensor of size 2048x8 (cuda:0)])
          (lora_magnitude_vector): ModuleDict()
        )
        (layers): ModuleList(
          (0-15): 16 x LlamaDecoderLayer(
            (self_attn): LlamaSdpaAttention(
              (q_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (k_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=512, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=512, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (v_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=512, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=512, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (o_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (rotary_emb): LlamaRotaryEmbedding()
            )
            (mlp): LlamaMLP(
              (gate_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=8192, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=8192, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (up_proj): lora.Linear(
                (base_layer): Linear(in_features=2048, out_features=8192, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=2048, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=8192, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (down_proj): lora.Linear(
                (base_layer): Linear(in_features=8192, out_features=2048, bias=False)
                (lora_dropout): ModuleDict(
                  (default): Identity()
                )
                (lora_A): ModuleDict(
                  (default): Linear(in_features=8192, out_features=8, bias=False)
                )
                (lora_B): ModuleDict(
                  (default): Linear(in_features=8, out_features=2048, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (lora_magnitude_vector): ModuleDict()
              )
              (act_fn): SiLU()
            )
            (input_layernorm): LlamaRMSNorm((2048,), eps=1e-05)
            (post_attention_layernorm): LlamaRMSNorm((2048,), eps=1e-05)
          )
        )
        (norm): LlamaRMSNorm((2048,), eps=1e-05)
        (rotary_emb): LlamaRotaryEmbedding()
      )
      (lm_head): lora.Linear(
        (base_layer): Linear(in_features=2048, out_features=128256, bias=False)
        (lora_dropout): ModuleDict(
          (default): Identity()
        )
        (lora_A): ModuleDict(
          (default): Linear(in_features=2048, out_features=8, bias=False)
        )
        (lora_B): ModuleDict(
          (default): Linear(in_features=8, out_features=128256, bias=False)
        )
        (lora_embedding_A): ParameterDict()
        (lora_embedding_B): ParameterDict()
        (lora_magnitude_vector): ModuleDict()
      )
    )
  )
)

 

더보기의 내용을 상세히 보면 모든 layer 에 lora adapter 가 생성됨을 알 수 있다.

그렇다면 lora adapter 를 학습하기 위한 Hyper-Parameter 로 가장 중요한 값인 lora_rank 와 lora_alpha 는 어떻게 셋팅할 수 있는지 확인해보자.


3. LoRA Config 설정 for Hyper-Parameter 셋팅

먼저 Hyper-Parameter 는 학습을 위한 yaml 파일을 통해 셋팅할 수 있다.

examples 예시를 보면 lora_rank 만 있다. 아는 사람은 다 알겠지만 lora_rank 만큼 중요한 값은 lora_alpha 다.

 

코드 내부를 확인해보니 lora_alpha 의 값을 지정하지 않으면 자동으로 lora_rank*2 의 값이 셋팅됨을 확인했다.

아래 부분을 확인해보면 알 수 있다.

  • LLaMA-Factory/src/llamafactory/hparams/parser.py 쪽에서 각 모듈(data, model, train, etc...) 별 arguments 들을 종합한다.
    • 상세한 내용은 _TRAIN_ARGS 의 값을 확인하기 바란다.
  • LoRA config 는 FinetuningArguments 에서 가져온다.
    • self.lora_alpha: int = self.lora_alpha or self.lora_rank * 2 를 통해 알 수 있듯 lora_rank 의 2배 값이 lora_alpha 가 된다. (단, lora_alpha 를 지정하지 않은 경우에만

4. 필자 리뷰

LLaMA-Factory 코드는 진짜 상당히 잘 짜여져 있다.

코드 구조도 가독성 좋게 구분되어 있어서 커스터마이징 하기도 좋다.

하지만 이를 사용하기 위해서는 구체적으로 코드의 구성이 어떻게 되어 있는지, 그것이 모델의 성능에 주는 효과가 무엇일지 확인할 수 있어야 한다. 만약 이 파악이 안 되어 있다면 딥러닝 전문가라 부르기 민망할 것이다. 

 

LLaMA-Factory 에서 파악한 몇 가지 정보를 추가 공유한다.

  1. max_length 셋팅을 어떻게 하는가?
    • yaml 파일에 packing: true 를 해주면 셋팅해준 cutoff_len+8 만큼 max_length 가 셋팅된다.
      • 왜 +8 만큼 늘어나는지는 직접 확인해보기 바란다.. (어차피 필자는 LLaMA-Factory 안 쓸 예정이라 파악하기 귀찮다...)
      • yaml 파일에 max_length 를 셋팅해주면 max_length 가 default value 인 2048 로 셋팅됨을 주의하기 바란다. cutoff_len 을 써야한다.
    • packing 에 의해 만약 +8 만큼 늘어나는게 싫다면 아래와 같이 해주면 될거다.
      • Trainer 쪽을 수정
      • 데이터셋을 tokenize 할 때 truncation 를 통해 max_length 만큼 PAD token 을 채워줌
    • 필자의 추천은 Trainer 쪽을 수정하는걸 추천한다. 그게 더 쉽다.
  2. Full Parameter tuning 은 어떻게 하는가?
    • model load 할 때 full fine-tuning 을 목적으로 하면 된다.
    • 간단하다. finetuning_type=full 로 셋팅하면 된다.
    • 해당 코드가 궁금한 분은 LLaMA-Factory/src/llamafactory/model/adapter.py 에서 init_adapter 함수를 참고하기 바란다.
  3. 원하는 Accelerate 사용을 어떻게 하는가? 
    • transformers 에서 config 설정을 통해 accelerate 를 쉽게 사용할 수 있는데 해당 방법을 선택했다.
    • adapter.py 에 보면 fsdp 및 deepspeed 등을 사용할 수 있는지 정도만 체크하고 위 방법대로 학습을 진행한다.
    • is_fsdp_enabled, is_deepspeed_zero3_enabled 를 위주로 참고해보라.

 

여기서 1번. max_length 셋팅 관련해서 추가 설명을 하고 본 포스팅을 마지막으로 LLaMA-Factory 훑어보기는 마무리하겠다. 먼저 해당 현상을 어떻게 파악했는지를 말하자면...

  • README.md 에 있는 example 로 max_length 셋팅에 대해 체크해봤다.
    • `llamafactory-cli train examples/train_lora/llama3_lora_sft.yaml` 실행
  • 처음엔 cutoff_len 가 max_length 인줄 알고 당연히 max_length 만큼 빈 공간은 PAD token 으로 채워줄 줄 알았다.
    • 하지만... cutoff_len 을 1024 로 설정해도 max_length 가 456 이 되더라..
    • cutoff_len 외에도 max_length 와 유사한 모든 arguments 를 셋팅해줬지만 현상은 동일했다.
    • max_length=456 이 된 이유는 데이터 중 가장 token 의 길이가 긴 것으로 max_length 가 456 으로 자동 셋팅된 것이다.
  • 데이터 load 하는 부분을 뜯어보니 packing 이라는 argument 가 있더라.
    • 하지만 max_length 가 cutoff_len+8 만큼 늘어나는 문제가 있다.
    • 예를들어 cutoff_len 을 512 로 셋팅하면 max_length 는 520 이 된다.

 

그렇다면 이 문제를 어떻게 해결할 수 있을까?

앞서 언급한 바와 같이 2가지 방법이 있다.

 

첫 번째 방법은 packing 을 사용하지 않고 tokenize 해줄 때 max_length 만큼 PAD token 을 채워주면 된다.

LLaMA-Factory 에서 get_dataset 함수의 output 은 dataset_module 으로 변수명을 지었는데 데이터셋을 tokenize 해줄 때 PAD token 을 만들어주지 않는다. 즉, tokenizer(batch_sentences, padding='max_length') 와 같이 하지 않는다는 의미다. 아래 사진을 보면 쉽게 알 수 있다.

PAD token 이 max_length 만큼 생성되지 않은 dataset_module

 

내가 원하는만큼 max_length 로 데이터셋을 구성하여 모델을 학습하고 싶다면 get_dataset 함수 쪽을 수정해줘야 한다. 타고타고 내려가다보면 SupervisedDatasetProcessor class 를 통해 tokenize 해주는데 이쪽을 수정해줘야 한다.

SupervisedDatasetProcessor class 는 LLaMA-Factory/src/llamafactory/data/processor/supervised.py 에 있다.

근데 이거 상세하게 파보면 진짜 여러 함수와 객체들이 연결되어 있다. 그래서 직접 수정이 쉽지 않다....

 

필자가 추천하는 방법은 두 번째 방법이다.

바로 Trainer 에서 PAD token 을 max_length 길이만큼 직접 추가해주는 방법이다. 이건 레퍼런스가 있기 때문에 적용하기 쉽다.

trl/trainer/sft_trainer.py 에서 SFTTrainer class 를 참고하면 된다.

해당 class 에서는 train_dataset 을 transformers 의 Trainer class 에 인자로 넣어주기 전 train_dataset 를 직접 수정해준다.

아래 코드를 참고하자.

        # Dataset
        preprocess_dataset = args.dataset_kwargs is None or not args.dataset_kwargs.get("skip_prepare_dataset", False)
        if preprocess_dataset:
            train_dataset = self._prepare_dataset(
                train_dataset, processing_class, args, args.packing, formatting_func, "train"
            )
            if eval_dataset is not None:
                packing = args.packing if args.eval_packing is None else args.eval_packing
                if isinstance(eval_dataset, dict):
                    eval_dataset = {
                        key: self._prepare_dataset(dataset, processing_class, args, packing, formatting_func, key)
                        for key, dataset in eval_dataset.items()
                    }
                else:
                    eval_dataset = self._prepare_dataset(
                        eval_dataset, processing_class, args, packing, formatting_func, "eval"
                    )

해당 코드에서 보면 train_dataset 을 _prepare_dataset method 활용하여 max_length 만큼 부족한 부분을 PAD token 으로 채워준다. 이렇게 채워준 뒤 Transformers 의 Trainer.py 에 train_dataset 인자를 넣어준다. 아래 코드를 참고하라.

        super().__init__(
            model=model,
            args=args,
            data_collator=data_collator,
            train_dataset=train_dataset,
            eval_dataset=eval_dataset,
            processing_class=processing_class,
            compute_loss_func=compute_loss_func,
            compute_metrics=compute_metrics,
            callbacks=callbacks,
            optimizers=optimizers,
            preprocess_logits_for_metrics=preprocess_logits_for_metrics,
            **super_init_kwargs,
        )

 

trl SFTTrainer class 와 같이 LLaMA-Factory 의 Trainer 인 CustomSeq2SeqTrainer class 에 코드를 추가해주면 된다.

추가만 해주면 사용자가 원하는 max_length 만큼 PAD token 을 추가하여 모델을 학습할 수 있다.


 

마무리,,

필자의 생각은....
진짜 간단하게 sLLM 을 학습하는걸 돌려보고 싶다는 니즈가 있다면 LLaMA-Factory 를 추천한다.
하지만 서비스에 적용하기 위한 모델 학습을 하고자한다면 LLaMA-Factory 사용은 지양하는걸 추천한다.
왜냐하면 목표한대로 모델을 학습하고자 하는 니즈가 있는 사람에게는 커스터마이징 해야 할 코드들이 다수 존재하기 때문이다.

하여 필자는 직접 sLLM 학습 코드를 구성하여 모델을 학습하는 방법을 추천한다.

 

이상으로 LLaMA-Factory 에 대한 코드 훑어보기 포스팅은 마치도록 하겠다.

댓글