[Elasticsearch] Efficient Data Enrichment Using Enrich Processor

Elasticsearch를 쓰다 보면 Index document에 대량으로 새로운 필드를 추가해야 하는 상황이 생긴다.
update-by-query나 bulk update를 시도하기엔 문서 수가 너무 많거나, 문서 하나하나를 업데이트해야 해서 작업 비용이 커지는 경우도 있다.

이런 문제를 해결하기 위해 선택한 기능이 바로 Enrich Processor이다.

Enrich를 사용하면 문서를 인덱싱하는 시점에 다른 인덱스의 참조 데이터를 자동으로 병합할 수 있어서, 복잡한 업데이트 작업 없이 깔끔하게 데이터를 보강할 수 있다.

Enrich란?

Enrich Processor는 인덱싱 과정에서 들어오는 문서를 대상으로, 미리 준비된 참조 데이터(reference data) 를 자동으로 붙이는 기능이다. 한마디로 “문서 저장 전에 실시간 lookup + 자동 join” 을 수행하는 구조이다.

Enrich 기능은 다음 3가지 요소로 구성된다.

Source Index참조 데이터가 들어 있는 인덱스 (예: 제품 정보, 사용자 정보)
Enrich Policy어떤 필드로 매칭하고 어떤 필드를 보강할지 정의
Enrich IndexPolicy 실행 시 생성되는 최적화된 시스템 인덱스 (.enrich-*)

Enrich Processor는 매번 검색을 수행하지 않고, 정적·최적화된 enrich index를 조회하기 때문에 성능 손실 없이 빠르게 보강 작업을 수행한다.

동작 예시

사용자의 이메일을 기반으로 해당 사용자의 기본 정보를 문서에 자동 보강하는 상황을 살펴본다.

1) Source Index 준비

PUT users/_doc/1?refresh=wait_for
{
  "email": "alice@example.com",
  "first_name": "Alice",
  "city": "Seoul"
}

2) Enrich Policy 생성

PUT /_enrich/policy/user-policy
{
  "match": {
    "indices": "users",
    "match_field": "email",
    "enrich_fields": ["first_name", "city"]
  }
}

3) Enrich Policy 실행 → enrich index 생성

POST /_enrich/policy/user-policy/_execute

여기까지 수행하면 .enrich-user-policy-* 형태의 전용 인덱스가 생성된다.

4) Ingest Pipeline 생성

PUT /_ingest/pipeline/user_lookup
{
  "processors": [
    {
      "enrich": {
        "policy_name": "user-policy",
        "field": "email",
        "target_field": "user_info",
        "max_matches": 1
      }
    }
  ]
}

5) 문서 인덱싱

PUT /logs/_doc/1?pipeline=user_lookup
{
  "email": "alice@example.com"
}

// 결과
{
  "email": "alice@example.com",
  "user_info": {
    "first_name": "Alice",
    "city": "Seoul"
  }
}

문서가 인덱싱될 때 자동으로 참조 데이터가 붙는 구조다.
update-by-query 없이 “인덱싱 순간에 데이터가 완성되는” 형태라고 보면 된다.

제한사항

❗ 자주 변경되는 참조 데이터를 Enrich로 처리하는 경우

Enrich Processor는 source index를 직접 조회하지 않고 Enrich Index를 조회하는 구조이다. 따라서 참조 데이터가 변경되면 enrich index를 재생성하는 작업을 반복하게 된다.

참조 데이터가 자주 바뀐다면 재생성 작업을 반복해야 하고, 그만큼 시스템 부하도 증가한다.
특히 대규모 인덱스에서는 재생성 비용이 커져 전체 ingest 성능에 영향을 줄 수 있다.

따라서 Enrich는 자주 변하지 않는 데이터인 경우에 사용하는 것이 적합하다.

[.NET]Using Transformers Models : Extending TransformersSharp

최근 프로젝트에서 텍스트/이미지 임베딩을 .NET 환경에서 직접 활용할 방법을 찾아보다가, TransformersSharp라는 프로젝트를 발견했다. 이 라이브러리는 Hugging Face의 transformers Python 라이브러리를 C#에서 직접 호출할 수 있도록 wrapping한 형태다. 덕분에 별도의 Python 서버를 띄우고 REST API를 붙이지 않아도 .NET 코드 안에서 곧바로 모델을 다룰 수 있다.

TransformersSharp는 기본적으로 텍스트 기반 임베딩과 파이프라인 기능들을 제공한다. 예를 들어 SentenceTransformer를 통해 문장을 벡터화할 수 있지만 이미지를 벡터화 할 수는 없다.

그래서 직접 TransformersSharp를 fork(Link)해서 기능을 다음과 같이 확장했다.

Python Wrapper

sentence_transformers_wrapper.py에 다음 함수를 추가했다.

def load_image(url_or_path):
    if url_or_path.startswith("http://") or url_or_path.startswith("https://"):
        return Image.open(requests.get(url_or_path, stream=True).raw)
    else:
        return Image.open(url_or_path)

def encode_image(model: SentenceTransformer, image_path: str) -> Buffer:
    """
    Encode an image using the SentenceTransformer model.
    """
    image = load_image(image_path)
    return model.encode(image)

C# Wrapper

SentenceTransformer.cs에는 다음 메서드를 추가했다.

public float[] GenerateImage(string image_path)
{
    var result = TransformerEnvironment.SentenceTransformersWrapper
        .EncodeImage(transformerObject, image_path);
    return result.AsFloatReadOnlySpan().ToArray();
}

이제 텍스트와 이미지를 같은 환경에서 임베딩할 수 있다.

using Microsoft.Extensions.AI;
using TransformersSharp;

var transformer_text = SentenceTransformer.FromModel(
    "sentence-transformers/clip-ViT-B-32-multilingual-v1", 
    trustRemoteCode: true);

var transformer_image = SentenceTransformer.FromModel(
    "clip-ViT-B-32", 
    trustRemoteCode: true);

var text_embeddings = transformer_text.GenerateSentence("A dog in the snow");
var image_embeddings = transformer_image.GenerateImage(
    "https://images.unsplash.com/photo-1547494912-c69d3ad40e7f?...");

Console.WriteLine($"Text Vector: {string.Join(", ", text_embeddings.Take(10))}...");
Console.WriteLine($"Image Vector: {string.Join(", ", image_embeddings.Take(10))}...");

Python 서버 없이도 .NET 안에서 바로 모델을 다룰 수 있다는 점이 굉장히 편리한 부분이 있었다. TransformersSharp 자체는 아직 완성도가 높은 수준은 아니지만, 필요한 기능을 확장할 수 있어서 오히려 테스트 해보기 좋았다.

[LLM] Experimenting with AutoTrain vs SFTTrainer

대규모 언어 모델을 Fine tuning할 때 HuggingFace에서는 AutoTrainSFTTrainer 두 가지를 사용 할 수 있다. AutoTrain은 CLI 기반으로 손쉽게 학습을 시작할 수 있는 도구이고, SFTTrainer는 파이썬 코드 기반으로 세밀한 제어가 가능한 도구이다.

AutoTrain은 학습 환경을 자동으로 구성해주기 때문에 초보자나 빠르게 결과를 확인하고 싶은 경우에 적합하다. 몇 가지 옵션만 주면 모델과 데이터 로딩, 학습 파라미터 세팅, 저장 경로까지 모두 알아서 설정해주는 것이 장점이다. 하지만 CLI 환경이라서 옵션의 유연성이 제한되고, 체크포인트 재개 기능이 옵티마이저 상태까지 복원되지 않는 제약이 있는 것이 단점이다.

SFTTrainer는 HuggingFace의 trl 라이브러리에서 제공하는 학습기반 클래스이다.
Trainer를 확장하여 Supervised Fine-Tuning에 맞게 최적화되어 있으며, resume_from_checkpoint 옵션을 통해 학습을 완전히 재개할 수 있는 것이 장점이다.
Data preprocessing, Training loop, Logging, Callback 등을 세밀하게 커스터마이징할 수 있기 때문에 연구 목적이나 장기 학습에서 강력한 도구이다. 다만 AutoTrain에 비해 설정을 직접 챙겨야 하므로 알아야 할 것이 많다.

AutoTrain의 CLI 실행 예시는 다음과 같다.

autotrain llm \
--train \
--model {base model name} \
--project-name {fine tuning model name} \
--data-path data/ \
--text-column text \
--lr 2e-4 \
--batch-size 1 \
--epochs 1 \
--block-size 512 \
--warmup-ratio 0.1 \
--lora-r 16 \
--lora-alpha 32 \
--lora-dropout 0.05 \
--weight-decay 0.01 \
--gradient-accumulation 8 \
--mixed-precision fp16 \
--peft \
--quantization int4 \
--trainer sft

위 명령어는 SFTTrainer 코드로 다음과 같이 변환할 수 있다.

from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments
from trl import SFTTrainer
from peft import LoraConfig, get_peft_model
from datasets import load_dataset

base_model = f"{base_model_name}"
finetuned_model = f"{fine_tuning_model_name}"

dataset = load_dataset("csv", data_files="data/train.csv")
train_dataset = dataset["train"]

tokenizer = AutoTokenizer.from_pretrained(base_model, use_fast=True)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(
    base_model,
    load_in_4bit=True,
    device_map="auto"
)

training_args = TrainingArguments(
    output_dir=finetuned_model,
    num_train_epochs=1,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=8,
    learning_rate=2e-4,
    weight_decay=0.01,
    warmup_ratio=0.1,
    logging_dir=f"{finetuned_model}/logs",
    logging_steps=50,
    save_strategy="steps",
    save_steps=200,
    save_total_limit=2,
    fp16=True,
    report_to="none",
)

peft_config = LoraConfig(
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=["q_proj", "k_proj", "v_proj"]
)

model = get_peft_model(model, peft_config)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=train_dataset,
    max_seq_length=512,
    packing=False,
    args=training_args,
)

trainer.train()

trainer.save_model(finetuned_model)
tokenizer.save_pretrained(finetuned_model)

이와 같이 AutoTrain CLI는 빠른 실험에 적합하고, SFTTrainer는 완전한 재개와 세밀한 제어가 필요한 상황에 적합하다. 상황에 따라 두 가지 도구를 병행하여 사용하는 것이 가장 효율적이다.

[LLM] Fixing AutoTrain’s use_flash_attention_2 TypeError

LLM 학습 중 아래처럼 TypeError가 터지면서 학습이 바로 종료되는 경우가 있다.

ERROR | 2025-08-27 13:47:10 | autotrain.trainers.common:wrapper:215 - train has failed due to an exception:
Traceback (most recent call last):
  ...
  File "/usr/local/lib/python3.12/dist-packages/transformers/modeling_utils.py", line 4999, in from_pretrained
    model = cls(config, *model_args, **model_kwargs)
TypeError: Exaone4ForCausalLM.__init__() got an unexpected keyword argument 'use_flash_attention_2'

오류가 발생하는 원인은 autotrain이 동작할때 내부적으로 AutoModelForCausalLM.from_pretrained() 호출 시 항상 use_flash_attention_2=config.use_flash_attention_2를 전달하는데 실제 원본 모델 클래스는 이 인자를 받지 않아 TypeError 발생하는 현상이다.

이 문제를 간단히 해결하는 방법은 autotrain 패키지의 실제 파일 위치(utils.py)에서 해당 인자 전달 한 줄을 주석 처리(또는 삭제)하는 것이다.

사용자의 환경에 따라 위치가 조금 다를 수 있지만 대략 적으로 autotrain utils.py 위치는 다음과 같다.

/usr/local/lib/python3.12/dist-packages/autotrain/trainers/clm/utils.py

위치를 확인했으면 다음과 같이 먼저 백업을 해두고 해당라인을 주석 처리 한다.

cp /usr/local/lib/python3.12/dist-packages/autotrain/trainers/clm/utils.py \
   /usr/local/lib/python3.12/dist-packages/autotrain/trainers/clm/utils.py.bak

sed -i 's/use_flash_attention_2=config.use_flash_attention_2/# use_flash_attention_2=config.use_flash_attention_2/' \
/usr/local/lib/python3.12/dist-packages/autotrain/trainers/clm/utils.py

grep -n "use_flash_attention_2" /usr/local/lib/python3.12/dist-packages/autotrain/trainers/clm/utils.py

# 848:            - use_flash_attention_2 (bool): Flag to use flash attention 2.
# 949:            # use_flash_attention_2=config.use_flash_attention_2,
# 957:            # use_flash_attention_2=config.use_flash_attention_2,

변경 사항이 제대로 반영되었는지 확인한 뒤 다시 실행하면, 오류가 해결된 것을 확인할 수 있다.

[LLM] Parameter-Efficient Fine-Tuning: LoRA and QLoRA

LLM은 수십억 개의 파라미터를 가지고 있기 때문에, 모든 파라미터를 학습시키는 Full Fine-Tuning 방식은 막대한 GPU 메모리와 시간이 필요하다. 특히 제한된 환경에서는 학습이 더 어려운데 이를 해결하기 위해 등장한 기법이 이를 해결하기 위해 등장한 기법이 PEFT(Parameter-Efficient Fine-Tuning)이다.

PEFT는 전체 파라미터를 학습하지 않고, 일부 파라미터만 학습하도록 하여 메모리 사용량과 연산량을 크게 줄인다. 그 결과, 학습 데이터가 적어도 효과적인 성능 향상을 기대할 수 있다.

LoRA (Low-Rank Adaptation)

LoRA는 모델의 기존 파라미터를 그대로 두고, 작은 보조 파라미터만 추가로 학습하는 방법이다. 기존의 Weight 행렬을 직접 업데이트하지 않고, 작은 두 개의 행렬 A, B를 학습하여 모델을 조정한다.

동작 원리

일반적인 선형 변환:

LoRA 적용 후:

  • W: 기존 모델 파라미터(freeze)
  • A, B: 학습할 작은 행렬
  • r: 행렬의 랭크(rank), 매우 작게 설정 (low-rank)

이 방식을 사용하면 내부 구조 rank가 낮아, 학습해야 하는 파라미터 수가 매우 적어진다.

이해하기 쉽게 예를 들어 보면, 전체 파라미터가 1024 * 1024로 총 1,048,576개인 모델이 있다고 가정해보자. r을 8로 해서 LoRA를 적용하면, A와 B행렬 각각이 8,192개의 파라미터만 가지게 되고 두 개를 합쳐도 16,384개의 파라미터만 학습하면 된다. 이는 전체 파라미터의 약 1.6%에 불과하다. 100만개가 넘는 파라미터 전부를 학습하는 대신 극히 일부만 학습해도 전부를 학습한것과 비슷한 학습 효과를 볼 수 있다.

QLoRA (Quantized LoRA)

QLoRA는 LoRA에 양자화(Quantization) 를 적용한 방식이다. 기존의 float32 파라미터를 int4와 같이 작은 비트수로 압축하여 GPU 메모리를 크게 절약한다.

특징

  • 최대 4배 이상 GPU 메모리 사용량을 감소할 수 있음.
  • 모델 본체는 작아지지만, optimizer state는 여전히 저장해야 하므로 경우에 따라 더 많은 메모리가 필요할 수 있음.
  • optimizer state는 전부 GPU에 올리지 않고, 필요할 때만 GPU로 이동하여 학습 하고 사용하지 않을 때는 CPU 메모리에 올려두도록 해야함.

[LLM] Understanding Encoder and Decoder

대형 언어 모델(LLM)에서 Encoder와 Decoder는 Transformer 구조의 두 핵심 구성 요소이다. 두 모듈은 서로 다른 역할과 구조적 특징을 가지고 있으며, 자연어 처리에서 입력을 이해하고 출력을 생성하는 데 중요한 역할을 한다.

Encoder

인코더는 입력 데이터를 처리하여 의미를 압축하고, 모델이 이해할 수 있는 벡터 표현으로 변환하는 역할을 한다.

구조적으로 Multi-Head Attention, Layer Normalization, Feed Forward Layer가 반복적으로 쌓인 형태다.

Encoder의 각 층 사이에는 Residual Connection이 있어, 입력 값을 변환 결과에 다시 더해 안정적인 학습이 가능하게 한다. Residual Connection은 깊은 네트워크에서 정보 손실을 방지하고, Vanishing Gradient 문제를 완화 한다.

입력 텍스트의 전후 관계를 이해하고, Context을 반영한 Embedding을 생성하고, 이 결과를 DecoderCross Attention단계에서 활용할 수 있게 한다.

Decoder

디코더는 인코더의 출력을 바탕으로 최종적으로 텍스트를 생성하는 역할을 한다.

구조적으로는 Encoder와 다르게 Multi-Head Attention대신 Masked Multi-Head Attention을 사용한다.

Cross Attention Layer를 통해서 Encoder의 결과를 입력으로 받아 사용한다.

토큰을 생성할 때, 사람의 글쓰기처엄 순차적으로 토큰을 하나씩 생성한다. 이미 생성된 토큰을 기반으로 다음 토큰을 예측한다.

학습할 때는 모두 완성된 텍스트를 입력 받는데 이 어텐션을 그대로 활용하는 경우 미래시점에 작성해야하는 텍스트를 미리 확인하게 되는 문제가 생긴다. 이런 문제를 막기위해 Masking을 적용한다.

[Elasticsearch] Understanding the Difference Between Fielddata and Keyword Fields

Elasticsearch를 사용하다 보면 집계나 정렬을 위해 텍스트 데이터를 처리해야 할 때가 많다. 이때 자주 등장하는 개념이 fielddatakeyword가 있는데 이 둘은 비슷한 목적(정렬, 집계)을 위해 사용되지만, 내부 동작 방식과 성능, 메모리 사용 면에서는 큰 차이가 있다.

fielddata (text 필드)

  • 기본적으로 text 타입은 검색에 적합하도록 분석(analyzed)된 문자열이다
  • 정렬이나 집계를 하기 위해선 fielddata를 사용하여 해당 데이터를 JVM 힙 메모리에 적재 한다.
  • 매우 메모리 집약적이며, 특히 대규모 데이터셋에서는 성능에 부담을 줄 수 있다.
  • 처음 한 번은 데이터를 읽어서 메모리에 올려야 하는데, 이때 리소스 사용량이 많다.

keyword type

    • keyword 타입은 분석되지 않은 raw 문자열로, 주로 정렬, 집계, 필터링에 사용된다.
    • doc values라는 구조로 디스크에 저장된다. 이 값은 인덱싱 시점에 미리 계산(precomputed) 되어 저장되며, 검색 시에는 디스크에서 읽어온다.
    • JVM 힙 메모리를 사용하지 않고, 일반적으로 fielddata보다 성능과 안정성 측면에서 유리하다.

    [LLM] What is layer normalization?

    딥러닝 모델을 학습할 때, 정규화(Normalization)는 빠르고 안정적인 학습을 위한 핵심 기법 중 하나다. 특히 자연어 처리(NLP)에서 사용되는 트랜스포머 기반 LLM(Large Language Model)에서는 층 정규화(Layer Normalization)가 중요한 역할을 한다.

    정규화는 딥러닝 모델에서 입력 데이터가 일정한 분포(평균과 분산)를 갖도록 조정해주는 기법이다. 정규화를 통해 모델은 다음과 같은 이점을 얻을 수 있다

    • 학습이 더 안정적이고 빠르게 진행 됨.
    • 과적합(Overfitting)방지에 도움 됨.
    • DNN(Deep neural network) architecture에서도 정보 흐름이 원활 함.

    Batch Normalization VS Layer Normalization

    Batch Normalization

    배치 정규화는 입력 데이터를 배치 단위로 묶어서 평균과 분산을 계산하고 정규화한다. 주로 이미지 처리 분야에서 사용되며, 자연어 처리에서는 아래 이미지 처럼 문장마다 길이가 다르더라도 일정한 길이로 맞추기 위해 PAD 토큰이 삽입된다.

    배치 마다 구성데이터가 달라서 정규화 효과가 일정하지 않게 되서 LLM에서는 일반적으로 Bath Norm을 사용하지 않는다.

    Layer Normalization

    층 정규화는 각 샘플(토큰 임베딩)마다 독립적으로 정규화한다. 같은 문장 안의 각 단어 벡터를 기준으로 평균과 분산을 계산한다.

    토큰 임베딩별 정규화를 수행하기 때문에 문장의 길이에 영향을 받지 않고 정규화를 동일하게 적용할 수 있다.

    LLM과 같은 트랜스포머 기반 자연어 처리 모델에서는, 입력의 다양성과 길이의 불균형을 고려해 배치 정규화 대신 층 정규화를 사용한다. 이는 모델이 더 빠르고 안정적으로 학습될 수 있도록 돕고, 문맥 이해 능력을 향상시키는 중요한 기법이다.

    [LLM] Self-Attention and Multi-head Attention

    자연어처리(NLP)에서 트랜스포머 모델은 언어의 문맥과 의미를 깊이 이해할 수 있게 해주는 혁신적인 구조이다. 그 중심에는 어텐션 있으며, 특히 Self-AttentionMulti-Head Attention이 있다.

    Attention이란?

    Aattention은 말 그대로 어디에 주의를 기울일 것인가를 계산하는 것이다.
    쉽게 설명하면, 우리는 평범한 글을 읽을 때는 왼쪽에서 오른쪽으로 자연스럽게 읽어 내려가지만, 어려운 글을 읽을 때는 특정 단어나 문장에 집중하며 앞뒤 내용을 다시 확인하곤 한다. 이러한 집중과 상호참조의 과정을 딥러닝 모델에 반영하기 위한 연산 방식이 바로 어텐션이다.

    Query, Key, Value

    Attention은 Q(Query), K(Key), V(Value)라는 세 가지 개념을 사용한다.

    • Query – 찾고자 하는 정보, 즉 ‘검색어’ 역할을 한다.
    • Key – 데이터가 어떤 쿼리와 관련이 있는지를 판단할 수 있게 해주는 특징 값. 예를 들어 문서 검색에서는 키가 문서의 제목, 본문, 저자일 수 있다.
    • Value – 관련 있는 데이터를 실제로 가져오는 값. 키를 통해 필터링된 후, 관련도에 따라 가중합되어 출력으로 사용됨.

    Self-Attention

    입력된 문장의 각 단어가 문장 내의 다른 단어들과 얼마나 관련이 있는지 계산하는 방식입니다. 입력된 문장의 모든 토큰을 Q, K, V로 변환한 뒤, 아래 그림과 같은 순서로 연산이 진행된다.

    Self-Attention
    1. Matrix product – Query와 Key의 내적해서 관련도를 측정한다.
    2. Sacling – 안정적인 학습을 위해 임베딩 차원수로 나눈다.
    3. Mask(Option) – Decoder 에서 미래의 토큰을 마스킹 할 것인지 선택하고 마스킹 적용.
    4. Softmax – 각 토큰 간 관련도 점수를 확률로 변환.
    5. Output – Value와 내적해서 최종 출력 생성.

    Multi-head Attention

    하나의 어텐션만 사용하는 것보다 여러 개의 어텐션을 병렬로 사용하면 더 다양한 관계를 동시에 파악할 수 있다.

    단순히 하나의 Attention으로만 문장을 이해하면 놓치는 정보가 생길 수 있다.
    그래서 여러 개의 head를 만들어서 서로 다른 방식으로 문장을 분석한 후, 그걸 합쳐서 더 풍부하고 정확한 표현을 얻는 방식이다.

    Multi-head Attention
    1. 입력을 여러 개의 Query, Key, Value를 head 크기로 분리하고 선형 변환을 한다.
    2. 각각의 헤드에서 Scaled Dot-Product Attention을 수행.
    3. 각 Attention 결과를 연결한다.
    4. 최종 선형 변환을 거쳐 결과 출력 한다.

    셀프 어텐션과 멀티헤드 어텐션은 트랜스포머가 자연어의 의미를 효과적으로 이해하게 해주는 핵심 기술이다. 문장 내에서 중요한 단어들 간의 관계를 파악하고, 다양한 시각에서 이를 종합하는 능력을 부여한다.

    [LLM] Model Parallelism and Data Parallelism

    대규모 언어 모델(LLM, Large Language Model)의 학습은 수십억 개 이상의 파라미터를 다루기 때문에, 단일 GPU의 메모리나 연산 능력으로는 처리하기 어렵다. 이러한 문제를 해결하기 위해 병렬화(parallelism) 전략이 도입되며, 주로 두 가지 방식인 모델 병렬화(Model Parallelism)데이터 병렬화(Data Parallelism)가 사용된다.

    Model Parallelism

    모델 병렬화는 하나의 모델을 여러 GPU에 분산시켜 학습하는 방식입니다. 모델이 너무 커서 단일 GPU 메모리에 올릴 수 없을 때 사용된다. 대표적인 두 가지 방식이 있다.

    1. Pipeline Parallelism
      • 딥러닝 모델의 층(Layer) 단위로 모델을 분할하고, 각 분할을 다른 GPU에 할당한다.
      • 입력 데이터가 GPU를 순차적으로 통과하며 forward/backward 연산이 수행된다.
      • 모델 크기를 효과 적으로 분산 할 수 있지만, 각 GPU가 순차적으로 동작하게 되어서 파이프라인 버블 문제가 생길 수 있다.
      • 그림에서 머신을 1, 2와 3, 4로 나누면 파이프라인 병렬화이다.
    2. Tensor Parallelism
      • 하나의 층 내부에서 계산되는 텐서 연산을 여러 GPU에 나누어 수행한다.
      • 행렬 곱 연산에서 행 또는 열을 나눠 병렬 연산 후 결과를 합치는 연산을 하면서 수행된다.
      • 레이어 수준보다 더 미세하게 병렬화할 수 있어서 계산량을 세밀하게 분산이 가능하지만, GPU간 통신이 빈번하게 발생하게 되어 통신 오버헤드가 커질 수 있음.
      • 그림에서 머신을 1, 3과 2, 4로 나누면 텐서 병렬화이다.
    Model Parallelism

    Data Parallelism

    모델 크기가 하나의 GPU에 올라가기 적당하고 데이터가 많을 경우 사용할 수 있는 방식으로 데 모델을 그대로 복제한 후, 서로 다른 GPU에서 다른 데이터를 학습시키는 방식이다.

    그림 처럼 각 GPU는 동일한 모델을 가지고 있으며, 각기 다른 미니배치 데이터를 학습한다.

    Data Parallelism

    forward/backward 연산 후, 파라미터의 그레이디언트(gradient) 를 모든 GPU가 서로 동기화하여 업데이트한다.

    구현이 간단하고 대부분의 프레임워크에서 지원이 되지만, 모델 크기가 GPU 메모리에 들어가야 하고, 모든 GPU 간의 gradient 통신 비용이 크다.

    LLM 모델 학습은 단순히 모델을 만드는 것 이상으로 효율적인 리소스 분산이 핵심이다. 모델 크기, 하드웨어 환경, 통신 병목 등을 고려하여 파이프라인 병렬화, 텐서 병렬화, 데이터 병렬화를 적절히 조합여 사용해야 한다.