광고
~ / posts / developer

어텐션 구현 고르기, SDPA·FlashAttention 메모리와 GPU 비용

어텐션 구현은 결과가 같고 메모리·속도만 달라요. Qwen3-8B에 16K 토큰을 넣으면 eager는 점수표만 48GiB라 H100이 필요하고, SDPA는 A100 40GB에 들어가요. 월 비용, is_causal 함정, 체크리스트까지 정리했어요.

Hugging Face 모델을 불러올 때 attn_implementation 에 "eager", "sdpa", "flash_attention_2" 중 무엇을 넣을지, 직접 짠 모델에서 F.scaled_dot_product_attention 을 그냥 믿어도 되는지 고민하고 계신가요? 결론부터 말하면 기본값인 sdpa 로 시작하고, 어텐션 가중치를 꺼내 봐야 할 때만 eager, sdpa 가 원하는 커널을 못 고를 때만 flash_attention_2 를 따로 설치하세요. 세 구현은 수학적으로 같은 값을 내고, 차이는 메모리와 속도에서만 나요. 그 차이가 입력 길이에 따라 GPU 등급을 바꿀 만큼 커져요.

이 글은 어텐션이 Q·K·V 로 어떻게 계산되는지는 안다고 보고, 구현을 고를 때 필요한 숫자만 다뤄요. Qwen3-8B 의 공개 설정값으로 길이별 메모리와 연산량을 직접 계산하고, 공식 가격표 단가로 월 비용을 낸 뒤, 직접 구현할 때 조용히 틀리는 인과 마스크 정렬 문제를 코드로 확인해요.

광고

어텐션 구현 종류와 차이 한눈에 보기

Hugging Face Transformers 의 어텐션 백엔드 문서는 첫 문장에서 모든 구현이 같은 계산을 한다고 밝혀요. 기본 구현은 어텐션 행렬 전체를 메모리에 펼치기 때문에 느리고, 최적화 구현은 계산 순서를 바꿔 메모리 왕복을 줄인다는 설명이에요. 정리하면 아래 표와 같아요.

구현 (attn_implementation)점수표를 만드나설치조건·특징
eager만들어요 (길이²)없음어텐션 가중치를 그대로 돌려줘요. 디버깅·분석용
sdpa (기본값)커널에 따라 안 만들어요없음 (PyTorch 내장)CUDA 에서 FlashAttention-2·메모리 효율·cuDNN·C++ 구현 중 자동 선택
flash_attention_2안 만들어요flash-attn 패키지Ampere·Ada·Hopper GPU, fp16·bf16, 헤드 차원 256 이하
flash_attention_3안 만들어요별도 빌드H100·H800, CUDA 12.3 이상
flex_attention블록 단위로 건너뛰어요없음 (PyTorch 내장)마스크·점수 변형을 파이썬 함수로 적고 컴파일

조건은 FlashAttention 공식 저장소 README에 적힌 내용이에요. FlashAttention-2 는 A100·RTX 3090·RTX 4090·H100 같은 Ampere 이후 GPU 를 지원하고, T4·RTX 2080 같은 Turing GPU 는 별도 저장소에서 일부 기능만 지원한다고 적혀 있어요. 자료형은 fp16·bf16 뿐이라 fp32 로 돌리는 모델에는 못 써요.

PyTorch scaled_dot_product_attention 문서(2.14 기준)는 세 구현(FlashAttention-2, 메모리 효율 어텐션, C++ 구현)이 기본으로 모두 켜져 있고, 입력을 보고 가장 빠른 것을 고른다고 설명해요. CUDA 가 아닌 장치에서는 C++ 구현만 써요.

어텐션 가중치가 필요하면 eager, 마스크가 인과·패딩·창 수준이 아니면 flex_attention, 그 외에는 sdpa 로 시작해 프로파일러로 flash 커널을 확인하는 판단 순서도
대부분은 sdpa 에서 끝나고, 예외 두 가지만 따로 고르면 돼요

eager 어텐션 메모리, 길이에 따라 얼마나 커지나

eager 가 무거운 이유는 코드에 그대로 보여요. Transformers 의 Qwen3 구현(eager_attention_forward)은 쿼리와 키를 곱해 점수표를 만들고, 그 표에 softmax(..., dtype=torch.float32) 를 건 뒤 다시 원래 자료형으로 바꿔요. 소프트맥스를 거는 순간에는 bf16 점수표(칸당 2바이트)와 fp32 결과(칸당 4바이트)가 함께 있어서, 칸당 최소 6바이트가 동시에 잡혀요.

점수표 칸 수는 배치 × 쿼리 헤드 수 × 길이² 예요. Qwen3-8B 의 config.json은 쿼리 헤드 32개, KV 헤드 8개, 헤드 차원 128, 층 36개예요. 배치 1로 직접 계산하면 다음과 같아요.

입력 길이bf16 점수표+ fp32 소프트맥스 (최소 피크)같은 길이의 KV 캐시 전체
4,0961GiB3GiB0.56GiB
8,1924GiB12GiB1.13GiB
16,38416GiB48GiB2.25GiB
32,76864GiB192GiB4.5GiB
Qwen3-8B 한 층의 eager 점수표 피크 vs 36층 KV 캐시 전체 (배치 1)
eager 점수표 피크KV 캐시 전체
0501001502004K · eager 점수표 피크 3GiB4K · KV 캐시 전체 0.56GiB4K8K · eager 점수표 피크 12GiB8K · KV 캐시 전체 1.1GiB8K16K · eager 점수표 피크 48GiB16K · KV 캐시 전체 2.2GiB16K32K · eager 점수표 피크 192GiB32K · KV 캐시 전체 4.5GiB32K

점수표는 한 층 분량이고 층이 끝나면 풀리지만, 그 순간의 피크가 GPU 에 들어가야 해요.

단위: GiB · 자료: Qwen3-8B config.json(쿼리 헤드 32·KV 헤드 8·헤드 차원 128·36층)으로 직접 계산, 점수표는 bf16 2바이트 + fp32 4바이트
표로 보기
구분eager 점수표 피크KV 캐시 전체
4K30.56
8K121.1
16K482.2
32K1924.5

눈여겨볼 점은 점수표 하나가 36층 KV 캐시 전체보다 수십 배 크다는 거예요. KV 캐시는 길이에 비례하고, 점수표는 길이 제곱에 비례하기 때문이에요. 8K 에서 이미 10배, 32K 에서는 40배가 넘어요. 학습에서는 역전파를 위해 층마다 저장해야 해서 훨씬 커져요.

FlashAttention 은 이 표를 작은 블록으로 잘라 GPU 칩 안의 SRAM 에서 처리하고, 행마다 최댓값과 합 같은 통계 몇 개만 남겨요. FlashAttention-2 논문 초록은 이 방식이 메모리를 길이 제곱이 아니라 길이에 비례하게 줄이면서 근사가 없다고 밝혀요. 32K 에서 32헤드 × 32,768행 × fp32 통계 하나를 쳐도 4MiB 수준이에요. GiB 와 GB 를 섞으면 계획이 7% 넘게 어긋나니, 헷갈리면 데이터 용량 변환기로 1024 기준과 1000 기준을 같이 보세요.

긴 입력에서 어텐션 연산이 차지하는 비중

메모리 말고 연산량은 어떨까요. 토큰 하나를 처리할 때 드는 연산은 가중치 행렬곱과 어텐션 점수 계산으로 나뉘어요. Qwen3-8B 의 임베딩 제외 가중치는 직접 계산하면 약 69.5억 개라, 토큰당 행렬곱 연산이 약 13.9 GFLOP 이에요. 어텐션은 위치 t 의 토큰이 앞 t 개 키와 점수를 내고(Q·Kᵀ) 값을 섞는(·V) 데 층마다 4 × 쿼리 헤드 × 헤드 차원 × t 만큼 들어요. 인과 마스크가 있으면 프롬프트 전체 평균은 t = 길이 ÷ 2 예요.

Qwen3-8B 프리필 토큰 하나당 연산량 (평균)
가중치 행렬곱어텐션 점수·가중합
05101520252K · 가중치 행렬곱 13.9GFLOP2K · 어텐션 점수·가중합 0.6GFLOP2K4K · 가중치 행렬곱 13.9GFLOP4K · 어텐션 점수·가중합 1.2GFLOP4K8K · 가중치 행렬곱 13.9GFLOP8K · 어텐션 점수·가중합 2.4GFLOP8K16K · 가중치 행렬곱 13.9GFLOP16K · 어텐션 점수·가중합 4.8GFLOP16K32K · 가중치 행렬곱 13.9GFLOP32K · 어텐션 점수·가중합 9.7GFLOP32K

어텐션 비중은 2K 4% → 8K 15% → 32K 41% 로 늘어요.

단위: GFLOP · 자료: Qwen3-8B config.json 으로 직접 계산 (LM 헤드 제외, 인과 마스크로 평균 길이의 절반만 계산한다고 가정)
표로 보기
구분가중치 행렬곱어텐션 점수·가중합
2K13.90.6
4K13.91.2
8K13.92.4
16K13.94.8
32K13.99.7

2K 이하 대화형 요청이면 어텐션은 5% 안팎이라 구현을 바꿔도 체감이 작아요. 수만 토큰을 넣는 RAG·코드 분석에서는 40% 를 넘어 구현 선택이 바로 지연에 드러나요. eager 는 점수표를 다 계산한 뒤 미래 칸을 지우는 방식이라 버릴 절반까지 계산하고, 블록 단위 커널은 가려진 블록을 건너뛸 수 있어요.

속도 차이는 논문 수치로 가늠해요. FlashAttention 논문(Dao 외, 2022) 초록은 GPT-2(길이 1K) 학습에서 3배, Long Range Arena(길이 1K~4K)에서 2.4배 빨라졌다고 보고해요. FlashAttention-2 는 여기서 다시 약 2배 빨라져 A100 이론 최대 연산 성능의 50~73% 에 닿았다고 적었어요. 학습 기준 측정이라 추론에 그대로 옮기면 안 돼요.

GPU 등급과 월 비용으로 바꿔 보기

메모리 피크는 어떤 GPU 를 빌려야 하는지를 정해요. Qwen3-8B 를 bf16 으로 올리면 가중치만 약 15.3GiB 예요. 배치 1, 입력 16K 를 메모리 기준으로만 맞춰 보면 이렇게 돼요.

8K 까지는 eager 도 약 28.4GiB 라 40GB 에 들어가고, 32K 는 점수표 피크만 192GiB 라 H100 한 장으로도 안 돼요.

Lambda GPU 클라우드 가격표의 1장 인스턴스 단가(2026-10-28 확인, 세금 별도)는 H100 SXM 80GB 시간당 4.29달러, A100 SXM 40GB 1.99달러, A10 24GB 1.29달러예요. 730시간(한 달) 상시 운영으로 계산했어요.

Qwen3-8B 16K 입력을 담는 최소 GPU의 월 비용 (메모리 기준)
eager · H100 80GB 1장eager · H100 80GB 1장 · 3,132달러3,132달러sdpa · A100 40GB 1장sdpa · A100 40GB 1장 · 1,453달러1,453달러

메모리만 맞춘 하한이에요. 처리량 목표가 있으면 GPU 수는 그걸로 다시 정해야 해요.

단위: 달러 · 자료: Lambda 가격표 1장 인스턴스 단가(2026-10-28 확인) × 730시간으로 직접 계산
표로 보기
구분월 비용
eager · H100 80GB 1장3,132
sdpa · A100 40GB 1장1,453

구현 이름 하나 때문에 GPU 등급이 한 단계 올라가고 월 1,679달러가 더 나가는 셈이에요. 가격표의 Quadro RTX 6000(시간당 0.69달러)처럼 Turing 세대를 고르면 FlashAttention-2 는 못 쓰니 sdpa 에 맡기세요. 서빙 단계의 동시 요청 수까지 따지려면 vLLM KV 캐시 용량 계산에서 정리한 블록 단위 계산을 이어서 하면 돼요.

KV 캐시를 끄면 생기는 일, use_cache 비용

구현만큼 비용을 크게 바꾸는 건 KV 캐시 사용 여부예요. 인과 마스크가 있는 디코더는 뒤에 토큰이 붙어도 앞 토큰들의 K·V 가 바뀌지 않아서, 저장해 두고 새 토큰 한 줄만 계산해도 결과가 같아요. Qwen3-8B 의 config.json 도 "use_cache": true 가 기본이에요.

커스텀 루프에서 실수로 캐시 없이 생성하면 매 스텝 앞 토큰 전체를 다시 처리해요. 입력 1,000토큰에서 500토큰을 생성하면 이래요.

방식선형층이 처리하는 토큰 수배수
KV 캐시 사용프리필 1,000 + 디코드 500 = 1,5001배
캐시 없이 매번 전체 재계산1,000 + 1,001 + … + 1,499 = 624,750416.5배

디코드는 메모리 대역폭에 묶여 있어서 시간 차이는 416배보다 작을 수 있지만 방향은 같아요. 캐시를 끄고 싶은 이유가 메모리라면, 캐시 크기를 줄이는 GQA 모델을 고르는 쪽이 훨씬 나아요. Qwen3-8B 는 KV 헤드가 8개라 토큰당 KV 가 144KiB 인데, 쿼리 헤드와 같은 32개였다면 576KiB 로 4배였을 거예요. GQA 를 처음 제안한 Ainslie 외 논문은 GQA 가 멀티헤드에 가까운 품질을 내면서 MQA 에 가까운 속도를 낸다고 보고해요.

광고
광고

is_causal 마스크 정렬, KV 캐시와 쓸 때의 함정

SDPA 를 직접 부르는 코드에서 가장 찾기 어려운 버그가 여기서 나와요. PyTorch 문서는 is_causal=True 일 때 정사각형 점수표에는 하삼각 마스크를, 정사각형이 아니면 왼쪽 위에 맞춘 마스크를 쓴다고 적어요. 반면 FlashAttention README 는 2.1 버전부터 causal=True 마스크를 오른쪽 아래에 맞춘다고 적어요.

프리필과 학습은 쿼리 수와 키 수가 같아서 두 정렬이 같은 결과를 내요. 문제는 KV 캐시로 디코드할 때예요. 쿼리는 새 토큰 1개, 키는 캐시에 쌓인 것까지 전부라서 점수표가 1 × S 직사각형이 돼요. 이 상황을 numpy 로 재현했어요(numpy 2.4.0, PyTorch 문서의 참조 구현과 같은 tril 정렬).

import numpy as np

rng = np.random.default_rng(1)
S, d = 5, 8                                   # 캐시 키 4개 + 새 토큰 = 키 5개
q_all, k, v = (rng.standard_normal((S, d)) for _ in range(3))

def attend(q, keep):                          # keep: True 인 칸만 참고
    s = np.where(keep, q @ k.T / np.sqrt(d), -np.inf)
    w = np.exp(s - s.max(-1, keepdims=True))
    w /= w.sum(-1, keepdims=True)
    return w @ v, w

upper_left   = lambda L, S: np.tril(np.ones((L, S), bool))          # SDPA is_causal
bottom_right = lambda L, S: np.tril(np.ones((L, S), bool), k=S - L) # flash-attn causal

full, _ = attend(q_all, upper_left(S, S))     # 캐시 없이 전체 계산한 기준값
for L in (1, 2):                              # 디코드 1개, 투기적 검증 2개
    for name, mask in (("upper-left", upper_left), ("bottom-right", bottom_right)):
        out, w = attend(q_all[-L:], mask(L, S))
        print(L, name, np.round(w[-1], 2), np.allclose(out, full[-L:]))
1 upper-left [1. 0. 0. 0. 0.] False
1 bottom-right [0.05 0.18 0.05 0.11 0.61] True
2 upper-left [0.22 0.78 0. 0. 0.] False
2 bottom-right [0.05 0.18 0.05 0.11 0.61] True

왼쪽 위 정렬에서는 새 토큰이 첫 번째 키만 보고 가중치 1을 몰아줘요. 오류도 경고도 없이 출력 모양까지 맞아서, 생성 문장이 이상하게 반복되거나 문맥을 잊는 증상으로만 드러나요. 쿼리가 2개인 경우(투기적 디코딩의 검증 단계나 청크 단위 프리필)도 마찬가지예요. 투기적 디코딩을 직접 붙인다면 투기적 디코딩 방식 고르기의 검증 단계가 바로 이 모양이에요.

새 토큰 하나와 캐시 키 다섯 개에서 PyTorch SDPA is_causal 은 첫 키만 남기고 flash-attn causal 은 다섯 키를 모두 남기는 마스크 비교
쿼리가 키보다 적을 때만 두 정렬이 갈라져요

해결은 간단해요. 쿼리 1개 디코드라면 마스크 없이(is_causal=False) 부르면 돼요. 새 토큰은 캐시의 모든 키를 봐야 하니까요. 쿼리가 여러 개라면 위치를 맞춘 불리언 마스크를 attn_mask 로 직접 넘기세요. 같은 문서는 attn_mask 와 is_causal 을 함께 주면 오류를 낸다고 적어요.

상황별 추천 구현

상황추천이유
처음 모델을 붙임, 입력 수천 토큰 이하sdpa (기본값)설치 없이 GPU 에 맞는 커널을 골라요
어텐션 가중치 시각화·헤드 분석eager, 짧은 입력으로fused 커널은 가중치 행렬을 만들지 않아서 꺼낼 게 없어요
16K 이상 긴 입력, Ampere 이후 GPUsdpa 에서 flash 커널이 잡히는지 확인 → 안 되면 flash_attention_2점수표가 GPU 등급을 바꿀 만큼 커져요
H100·H800 에서 처리량 최우선flash_attention_3 까지 측정Hopper 전용 최적화예요
문서 단위 마스크·슬라이딩 창·커스텀 점수 편향flex_attentionFlexAttention 문서처럼 score_mod·block_mask 함수로 적어요
T4·Turing 이하, fp32 필요sdpaFlashAttention-2 지원 대상이 아니에요
수치 재현성이 중요한 검증sdpa + C++ 구현 강제, 또는 float64커널마다 부동소수점 결과가 조금씩 달라요

vLLM 같은 서빙 엔진은 자체 백엔드를 고르니 이 표는 Transformers·직접 구현용이에요. 길이를 RoPE 스케일링으로 늘리는 중이라면 LLM 컨텍스트 길이 늘리기의 비용 계산과 함께 보세요. 늘린 길이만큼 위 점수표 계산이 제곱으로 따라와요.

도입 체크리스트

광고
광고

자주 묻는 질문

sdpa 와 flash_attention_2 결과가 정말 같나요?

수학적으로 같은 식을 계산하고 근사도 없어요. 다만 블록 순서와 합산 순서가 달라서 bf16 에서는 마지막 자리 정도의 차이가 나요. PyTorch 문서도 백엔드에 따라 출력이 다를 수 있다고 적어요. 생성 결과가 아주 가끔 다른 토큰으로 갈라지는 건 이 때문이고, 품질 차이는 아니에요.

맥북에서 sdpa 가 eager 보다 별로 안 빨라요. 설정이 잘못됐나요?

정상이에요. 문서상 CUDA 가 아닌 장치에서는 sdpa 도 C++ 기본 구현을 써요. 점수표를 만드는 방식이 eager 와 크게 다르지 않아서 차이가 작아요. 구현 선택은 실제 배포할 NVIDIA GPU 에서 측정해 정하세요.

flash-attn 패키지 설치가 계속 실패해요.

README 기준 Linux, PyTorch 2.2 이상, CUDA 또는 ROCm 툴킷, ninja 가 필요해요. Transformers 문서는 Kernels 라이브러리로 허브에서 컴파일된 커널을 내려받는 방법도 안내해요. 그 전에 sdpa 가 이미 flash 커널을 쓰고 있는지부터 확인하면 설치가 필요 없는 경우가 많아요.

어텐션 가중치를 보려면 꼭 eager 여야 하나요?

FlashAttention 계열은 점수표를 블록 단위로 계산하고 버리기 때문에 꺼낼 가중치 행렬이 없어요. 가중치가 필요하면 eager 로 바꾸되, 위 표처럼 길이 제곱으로 메모리가 늘어나니 짧은 입력과 작은 배치로 분석하세요. Transformers 는 set_attn_implementation() 으로 모델을 다시 불러오지 않고 구현을 바꿀 수 있어요.

2K 토큰 챗봇인데도 구현을 바꿀 가치가 있나요?

메모리로는 점수표가 수백 MiB 수준이라 GPU 등급이 바뀌지 않고, 연산 비중도 5% 안팎이에요. 기본값 sdpa 그대로 두고 배치 크기, KV 캐시, 양자화 같은 다른 설정을 먼저 보는 편이 효과가 커요.

정리

메모리 단위는 데이터 용량 변환기로 맞추세요.

더 깊이 읽기

이 글은 실무에서 판단할 때 필요한 것 위주로 정리했어요. 같은 주제의 내부 동작 원리는 devboi 기술 블로그의 트랜스포머 어텐션 원리 — Q·K·V 와 LLM 다음 토큰 선택에서 도식과 코드로 단계별로 풀었어요.

참고한 자료

#어텐션 구현 비교#attn_implementation 설정#PyTorch SDPA 백엔드#FlashAttention 2 설치 조건#eager 어텐션 메모리#is_causal 마스크 정렬#KV 캐시 use_cache#LLM 추론 GPU 비용
← 이전 글스프링 시큐리티 JWT 리소스 서버 설정 판단표, 401·403 확인 순서다음 글 →코딩 에이전트 비용 계산, 한 번 생성·고정 파이프라인·도구 루프 고르는 법
광고

더 둘러보기

💻 개발자 다른 도구

🔥 많이 찾는 도구

🔮 재미로 한 번 더

📝 읽어 볼 만한 글

글 전체 보기 →