LLM 오픈소스 중 가장 유명한 LLaMA-Factory 를 파악하고 있다.
그 중 학습 시 LoRA 를 활용한다면 어떤 Adapter 를 생성하여 학습하는지 확인했다.
본 포스팅은 오픈소스인 LLaMA-Factory 에서 LoRA 활용한 학습 시 Adapter 및 arguments 셋팅이 어떻게 되어 있는지 코드를 통해 확인한 결과를 소개한다.
목차
1. 실무자는 바쁘다.! 결론부터 말씀드릴게요.
2. 생성되는 LoRA Adapter 확인 및 커스터마이즈
3. LoRA Config 설정 for Hyper-Parameter 셋팅
4. 필자 리뷰
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 에서 파악한 몇 가지 정보를 추가 공유한다.
- 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 쪽을 수정하는걸 추천한다. 그게 더 쉽다.
- yaml 파일에 packing: true 를 해주면 셋팅해준 cutoff_len+8 만큼 max_length 가 셋팅된다.
- Full Parameter tuning 은 어떻게 하는가?
- model load 할 때 full fine-tuning 을 목적으로 하면 된다.
- 간단하다. finetuning_type=full 로 셋팅하면 된다.
- 해당 코드가 궁금한 분은 LLaMA-Factory/src/llamafactory/model/adapter.py 에서 init_adapter 함수를 참고하기 바란다.
- 원하는 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') 와 같이 하지 않는다는 의미다. 아래 사진을 보면 쉽게 알 수 있다.
내가 원하는만큼 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 에 대한 코드 훑어보기 포스팅은 마치도록 하겠다.
'Python > 패키지 훓어보기' 카테고리의 다른 글
[PyTorch] nn.Transformer 모델 구조 상세 확인 (0) | 2025.03.12 |
---|---|
[LLaMA-Factory] Tokenizer padding_side 확인 (0) | 2025.02.22 |
[LLaMA-Factory] PT&SFT 학습 데이터는 어떻게 만들어지는가? (1) | 2025.01.22 |
댓글