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

[LLM] Strategies to Reduce GPU Memory Usage During LLM Training

대규모 언어 모델(LLM)을 학습하거나 미세 조정할 때 가장 큰 제약 중 하나는 GPU 메모리 부족이다. 학습 과정에서 어떤 데이터가 메모리에 올라가는지, 이를 어떻게 줄일 수 있는지 정리해 보자.

GPU 메모리에 저장되는 주요 데이터

  1. Model Parameters
    • 모델 가중치(Weight)와 편향(Bias) 등의 학습 가능한 값.
    • 예를 들어 7B (7 Billion) 파라미터 모델의 경우 FP32 기준 약 28GB 메모리가 필요함.
  2. Gradient
    • 각 파라미터에 대해 손실 함수의 기울기 값
    • 역전파(Back propagation) 과정에서 계산되며, 파라미터와 같은 크기의 메모리를 필요로 함.
  3. Optimizer State
    • Optimizer는 모델을 학습시키기 위해 파라미터를 업데이트 하는 알고리즘임. 이때 대부분의 고급 옵티마이저는 각 파라미터마다 추가적인 정보를 저장하는데 이 정보들이 Optimizer State임.
    • 예를 들어 Adam 옵티마이저는 각 파라미터에 대해 1st moment (이전 기울기의 평균), 2nd moment (기울기 제곱의 평균) 추적 값을 저장함.
    • 파라미터 크기의 2~3배에 달하는 추가 메모리 소모가 발생할 수 있음.
  4. Forward Activation
    • 순전파(Forward pass)에서 중간 레이어의 출력값.
    • 역전파 시 Gradient를 계산하기 위해 반드시 필요한 값.
    • 가장 많은 메모리를 차지할 수 있음.

메모리 절약을 위한 전략

  1. Gradient Accumulation
    • GPU 메모리 한계를 넘지 않기 위해 작은 배치(batch) 크기로 여러 번 계산하고 그래디언트를 누적(accumulate).
    • 일정 스텝마다 누적된 그래디언트를 이용해 파라미터를 한 번 업데이트함.
    • Batch size 16에서 OOM이 발생할 경우
      -> Batch size 4 (gradient_accumulation_steps=4)로 설정.
      -> Loss를 4로 나누어 누적하면 batch size 16과 동일한 학습 효과를 얻을 수 있음.
      -> 실제 메모리 사용량은 Batch size 4로 계산 됨.
      -> 작은 Batch size로 큰 Batch size로 학습로 학습한 것과 동일한 효과를 얻을 수 있지만 누적 연산을 추가로 해야 해서 학습 시간은 증가됨.
  1. Checkpointing (Activation Checkpointing)
    • 순전파 시 전체 activation을 저장하지 않고, 마지막 출력값만 저장.
    • 역전파 시 필요한 이전 상태는 다시 계산하여 사용.
    • 순전파 상태 값을 모두 저장하지 않아서 메모리 사용량은 대폭 감소되지만, 역전파 시 순전파를 매번 반복 계산하므로 연산량 증가하여 학습 시간은 증가됨.
  2. Gradient Checkpointing
    • 순전파의 일부 중간 지점(checkpoint)만 저장하고 나머지는 역전파 시 재계산.
    • 전체를 저장하는 일반 방식과, 끝만 저장하는 기본 checkpointing의 중간 전략.
    • 메모리 효율과 연산 효율을 적절히 절충한 방식이라 약간의 오버헤드는 존재하지만 일반적인 Checkpointing 보다는 효율적임.

GPU 메모리는 LLM 학습에서 가장 중요한 자원 중 하나이다. 위에서 소개한 전략들을 적절히 활용하면 제한된 자원 내에서도 효율적으로 모델을 학습시킬 수 있다.

[LLM] Floating Point Formats

AI 모델 학습과 추론에서 부동소수점(Floating Point) 형식은 성능과 정확도에 큰 영향을 준다. 특히, 연산 속도와 메모리 사용량을 줄이기 위해 다양한 정밀도의 부동소수점 형식이 사용되는데, 대표적으로 사용되는 BF16, FP32, FP16에 대해 간단히 정리해 본다.

  1. FP32 (Single Precision Floating Point)
    • Exponent – 8 Bit
    • Mantissa – 23 Bit
    • Sign – 1 Bit
    • Total 32 Bit
    • 높은 정밀도와 넓은 표현 범위를 모두 갖춘 표준 부동소수점 형식. 대부분의 AI 학습, 고정밀 연산이 필요한 경우 사용한다.
  2. FP16 (Half Precision Floating Point)
    • Exponent – 5 Bit
    • Mantissa – 10 Bit
    • Sign – 1 Bit
    • Total 16 Bit
    • 정밀도와 표현 범위 모두 FP32보다 줄어들지만, 메모리 절약과 연산 속도는 증가 시킬 수 있다.
  3. BF16 (bfloat16, Brain Floating Point Format)
    • Exponent – 8 Bit
    • Mantissa – 7 Bit
    • Sign – 1 Bit
    • Total 16 Bit
    • FP16과 마찬가지로 16 Bit를 사용하여 표현 범위가 줄어들었지만, FP32와 동일한 지수부를 가져서 표현 가능한 수의 범위가 넓다.

[Elasticsearch] Collecting RSS information with Logstash

RSS feed는 웹사이트의 최신 콘텐츠를 효율적으로 수집하고 분석할 수 있는 강력한 도구입니다. Logstash를 사용하면 RSS 데이터를 쉽게 수집하고 처리할 수 있습니다. 이 글에서는 Google News RSS를 Logstash로 수집하는 방법을 예제로 설명하겠습니다.

Logstash config

Logstash의 설정 파일은 input, filter, output 세 가지 섹션으로 구성됩니다. 아래는 Google News의 RSS 피드를 수집하기 위한 예제 설정입니다.

Input

http_poller 플러그인을 사용해 RSS 피드를 주기적으로 요청합니다.

input {
  http_poller {
    urls => {
      google_news => {
        url => "https://news.google.com/rss"
        method => get
      }
    }
    request_timeout => 60
    schedule => { every => "10m" }
    codec => "plain"
    metadata_target => "http_metadata"
  }
}

urls: 수집할 RSS 피드 URL을 지정합니다.
schedule: 데이터를 가져올 주기를 설정합니다. every => “5m”은 5분마다 데이터를 요청합니다. cron 표현식을 사용하여 보다 세부적인 설정이 가능합니다
metadata_target: 요청 메타데이터를 저장할 필드를 지정합니다.

Fiter

수집된 RSS 데이터를 구조화된 형태로 변환합니다.

filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  split {
    field => "[rss][channel][item]"
  }
  mutate {
    add_field => {
      "title" => "%{[rss][channel][item][title]}"
      "link" => "%{[rss][channel][item][link]}"
      "source" => "%{[rss][channel][item][source][content]}"
      "pubDate" => "%{[rss][channel][item][pubDate]}"
      "type" => "%{[http_metadata][input][http_poller][request][name]}"
    }
    remove_field => ["message", "rss", "event", "http_metadata"]
  }
}

xml: RSS 데이터를 XML 형태에서 파싱하여 rss 필드에 저장합니다.
split: 각 기사(item)를 개별 이벤트로 분리합니다.
mutate: 원하는 필드(title, link, source, pubDate)를 추가하고 불필요한 데이터를 제거합니다.

Output

결과 데이터를 Elasticsearch에 저장하거 콘솔에 출력합니다.

output {
  elasticsearch {
    hosts => ["http://localhost:9200"]
    index => "rss-data-%{+YYYY.MM.dd}"
  }
  stdout { codec => rubydebug }  
}

이 예제를 사용하면 Logstash를 활용하면 RSS 데이터를 효율적으로 수집하고 처리할 수 있습니다.

[Azure SQL]Using Always Encrypted with .NET (feat. Keyvault)(2)

[Azure SQL]Using Always Encrypted with .NET (feat. Keyvault)(1)에서 SQL에 Always Encryted가를 설정하여 데이터를 암호화하고 쿼리하는 방법에 대해서 알아봤다. 이번 포스트에서는 Always Encryted가 설정된 SQL를 이용하여 .Net 서비스를 개발하는 방법에 대해서 알아보겠다.

.Net 서비스 내에서 Always Encrypted가 설정된 SQL에 접근 하기 위해서는 Connection String에 아래 옵션이 설정 되어야 한다.

  • Encryption Setting=Enabled
  • Encrypt=True

appsettings.json에서 ConnectionStrings에 다음과 같이 설정하면 된다.

"ConnectionStrings": {
  "DefaultConnection": "Server=tcp:azure sql.database.windows.net,1433;Initial Catalog=DB;Persist Security Info=False;User ID={user id};Password={password};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;Column Encryption Setting=Enabled;"
}

여기까지 하면 db connection이 생성될 때 Always Encryed 설정이 활성화 된다. 하지만 CEK가 설정된 Table을 읽어 오지는 못한다. 제대로 값을 읽어 오기 위해서는 CEK Provider를 서비스 시작 시점에 설정을 해주어야 한다.

CEK Provider는 Program.cs에 다음과 같이 Provider를 구성해주면 된다.

var azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new DefaultAzureCredential());

var dics = new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>();
dics.Add(SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider);

SqlConnection.RegisterColumnEncryptionKeyStoreProviders(dics);

이제, 아래와 같이 간단히 앞서 암호화 했던 Users 테이블을 select해 보면 복호화 된 데이터를 확인 할 수 있다.

await using var conn = (SqlConnection)_context.Database.GetDbConnection();

await conn.OpenAsync();

await using var cmd = new SqlCommand("select * from Users", conn);
await using var reader = await cmd.ExecuteReaderAsync();

await reader.ReadAsync();

var result = $"{reader[0]}, {reader[1]}, {reader[2]}";
//id, email, name, registered date
//1, abc@gmail.com, abc, 2023-01-01 00:00.000

[Azure SQL]Using Always Encrypted with .NET (feat. Keyvault)(1)

데이터를 저장 및 관리 하다 보면, 민감한 정보를 아무나 볼 수 없도록 식별 불가능하게 하도록 비식별화 작업을 해야 할 때가 있다.

데이터 비식별화를 하는 방법은 여러가지가 있지만, 그 중에서 데이터를 암호화하는 방법을 Azure SQL의 Always Encrypted 기능을 사용해서 구현해보려고 한다.

Always Encryted 기능은 Azure SQL에 저장된 민감한 데이터를 암호화 시킬 수 있고, 암호화에 사용된 암호화 키를 DB engine과 공유하지 않기 때문에 권한이 있는 클라이언트만 실제 데이터를 확인 할 수 있다. 그래서 권한에 따라서 데이터를 볼 수 있는 사람과 없는 사람을 명확히 구분할 수 가 있게 된다.

구성 방법

Always Encryted 기능을 사용하기 위해서는 2가지 키가 필요하다.

  1. Column master key(CMK)
    • Column encryption key들을 암호화하는데 사용된다.
    • HSM 모듈을 사용해서 생성하고 Azure Keyvault에 저장한다.
  2. Column encrytion key(CEK)
    • Table의 특정 Column data를 암호화 하거나 복호화 하는데 사용된다.
    • 각 Column 별로 독립적으로 관리된다.

Create CMK

keyvault서비스에서 Key menu에서 Generate/Import 메뉴를 클릭한다.

사용할 RSA 키 정보를 입력하고 키를 생성한다.

  • Name: always-encryted-hsm
  • Key Type: RSA
  • Key Size: 3072

SQL Server Management Studio(SSMS)를 실행시킨다.
Object Explore에서 DB Instance를 선택하고, Security 폴더로 이동한다.
Always Encryted Key 폴더 밑에 Column master key폴더를 우클릭하여 master key 추가 메뉴를 클릭한다.

key store를 auzre keyvault를 선택하면, AAD Login을 해야한다. 정상적으로 로그인 하게 되면, subscription과 keyvault 리소스를 선택할 수 있다. 위에서 만든 RSA 키 정보를 찾아서 추가해준다.

여기까지 하면 Column master key가 생성된 것을 확인 할 수 있다.

Create CEK

이제 암호화를 대상이 되는 테이블을 우클릭 하면 Column Encryption key를 추가할 수 있는 메뉴를 확인 할 수 있다.

암호화를 해야 하는 Column들을 선택하고 위에서 만들 Master key를 사용해서 Column들을 암호화 한다.

이제 Table을 조회해 보면 해당 Column(email, name)의 정보가 식별할 수 없도록 암호화된 것을 확인 할 수 있다.

이 정보를 다시 복호화 하기 위해서는 SQL master 계정이나 Keyvault에 Cryptographic Operations 권한이 있는 계정을 사용해야 하며 Connection 정보에 “Column Encryption Setting = Enabled” 옵션을 추가해서 접근을 하면 복호화된 정보를 얻을 수 있다.

Next: [Azure SQL]Using Always Encrypted with .NET (feat. Keyvault)(2)

MS-SQL Stored procedure에 List(or Array) parameter 전달하기

Custom type을 정의하면 Stored procedure(이하 SP)에 List(or Array) 형태의 정보를 전달하여 처리 할 수 있다.

먼저, List로 전달할 정보를 담을 Custom table type을 정의한다.
객체 정보를 담는 Class를 정의한다고 생각하면 된다.

CREATE TYPE CodeList
AS TABLE
(
  Code varchar(32)  
);

SP에 변수를 type을 위에서 만든 Custom type으로 정의한다. 그러면, SP는 정의된 Table 정보를 받아 처리 할 수 있게 된다.

CREATE PROCEDURE GetInfoByCodes
	@pCodeList AS CodeList READONLY
AS 
BEGIN
	SET NOCOUNT ON;
	SELECT * FROM InfoTable
	WHERE Code in (SELECT * FROM @pCodeList)	
END
GO

임의로 Custom table type으로 만든 변수를 만들어 다음과 같이 SP를 실행해 볼 수 있다.

DECLARE @pCodes CodeList
INSERT INTO @pCodes VALUES('A40274208')
INSERT INTO @pCodes VALUES('A10028014') 
INSERT INTO @pCodes VALUES('A56087115') 

EXEC GetInfoByCodes @pCodeList = @pCodes