Transformers로 Qwen 추론하기

Transformers로 Qwen 추론하기

Transformers는 추론과 훈련을 위한 사전 훈련된 자연어 처리 라이브러리예요. 개발자는 Transformers로 자신의 데이터에 모델을 훈련하고, 추론 애플리케이션을 만들고, 대규모 언어 모델로 텍스트를 생성할 수 있어요.

출처: 문서

본문

환경 설정

  • transformers>=4.51.0
  • torch>=2.6 권장
  • GPU 권장

기본 사용법

transformers에서 Qwen3로 텍스트를 생성하려면 pipeline() 인터페이스나 generate() 인터페이스를 사용할 수 있어요.

일반적으로 pipeline 인터페이스는 보일러플레이트 코드가 덜 필요해요 (아래 참고). 다음은 다중 턴 대화에 pipeline을 사용하는 기본 예시예요.

from transformers import pipeline

model_name_or_path = "Qwen/Qwen3-8B"

generator = pipeline(
    "text-generation",
    model_name_or_path,
    torch_dtype="auto",
    device_map="auto",
)

messages = [
    {"role": "user", "content": "Give me a short introduction to large language models."},
]
messages = generator(messages, max_new_tokens=32768)[0]["generated_text"]
# print(messages[-1]["content"])

messages.append({"role": "user", "content": "In a single sentence."})
messages = generator(messages, max_new_tokens=32768)[0]["generated_text"]
# print(messages[-1]["content"])

pipeline을 만들 때 중요한 파라미터가 몇 가지 있어요.

모델: model_name_or_path는 Qwen/Qwen3-8B 같은 모델 ID이거나 로컬 경로일 수 있어요.

모델 파일을 로컬 디렉터리에 다운로드하려면 다음을 사용할 수 있어요:

huggingface-cli download --local-dir ./Qwen3-8B Qwen/Qwen3-8B

중국 본토에 있다면 ModelScope로 모델 파일을 다운로드할 수도 있어요:

modelscope download --local_dir ./Qwen3-8B Qwen/Qwen3-8B

디바이스 배치: device_map="auto"는 가능하면 모델 파라미터를 여러 디바이스에 자동으로 로드해요. 이는 accelerate 패키지에 의존해요. 단일 디바이스를 사용하려면 device_map 대신 device를 전달할 수 있어요. device=-1 또는 device="cpu"는 CPU 사용, device="cuda"는 현재 GPU 사용, device="cuda:1" 또는 device=1은 두 번째 GPU 사용을 뜻해요. device_map과 device를 동시에 사용하지 마세요!

계산 정밀도: torch_dtype="auto"는 체크포인트의 원본 정밀도와 여러분의 디바이스가 지원하는 정밀도에 따라 사용할 데이터 타입을 자동으로 결정해요. 최신 디바이스에서는 bfloat16으로 결정돼요. torch_dtype="auto"를 전달하지 않으면 기본 데이터 타입은 float32이며, 이는 메모리를 두 배로 차지하고 계산도 느려요.

텍스트 생성 pipeline 호출은 모델 파일의 생성 설정(예: generation_config.json)을 사용해요. 이 설정은 호출에 인자를 직접 전달해 덮어쓸 수 있어요. 기본값은 다음과 동일해요:

messages = generator(messages, do_sample=True, temperature=0.6, top_k=2, top_p=0.95, eos_token_id=[151645, 151643])[0]{"generated_text"}

생성 파라미터 구성의 모범 사례는 모델 카드를 참고하세요.

Thinking & Non-Thinking 모드

기본적으로 Qwen3 모델은 응답하기 전에 먼저 생각해요. pipeline() 인터페이스에서도 마찬가지예요. thinking과 non-thinking 모드를 전환하는 두 가지 방법이 있어요.

방법 1: thinking\n\n response\n\n만 포함하는 마지막 assistant 메시지를 추가하세요. 이 방식은 상태가 없어서(stateless) 그 한 턴에만 동작해요. 또한 모델이 thinking 콘텐츠를 생성하는 것을 엄격히 막아요. 예를 들어:

messages = [
    {"role": "user", "content": "Give me a short introduction to large language models."},
    {"role": "assistant", "content": " thinking\n\n response\n\n"},
]
messages = generator(messages, max_new_tokens=32768)[0]["generated_text"]
# print(messages[-1]["content"])

messages.append({"role": "user", "content": "In a single sentence."})
messages = generator(messages, max_new_tokens=32768)[0]["generated_text"]
# print(messages[-1]["content"])

방법 2: 사용자(또는 시스템) 메시지에 /no_think를 추가해 thinking을 끄고, /think를 추가해 thinking을 켜세요. 이 방식은 상태가 있어서(stateful), 다중 턴 대화에서 모델이 가장 최근의 지시를 따라요.

messages = [
    {"role": "user", "content": "Give me a short introduction to large language models./no_think"},
]
messages = generator(messages, max_new_tokens=32768)[0]["generated_text"]
# print(messages[-1]["content"])

messages.append({"role": "user", "content": "In a single sentence./think"})
messages = generator(messages, max_new_tokens=32768)[0]["generated_text"]
# print(messages[-1]["content"])

Thinking 콘텐츠 파싱

더 구조화된 assistant 메시지 형식을 원한다면, 다음 함수로 thinking 콘텐츠를 vLLM, SGLang 등에서 사용하는 것과 유사한 reasoning_content 필드로 추출할 수 있어요.

import copy
import re

def parse_thinking_content(messages):
    messages = copy.deepcopy(messages)
    for message in messages:
        if message["role"] == "assistant" and (m := re.match(r" thinking\n(.+) response\n\n", message["content"], flags=re.DOTALL)):
            message["content"] = message["content"][len(m.group(0)):]
            if thinking_content := m.group(1).strip():
                message["reasoning_content"] = thinking_content
    return messages

도구 호출 파싱

Transformers에서 도구 호출을 사용하려면 우리의 함수 호출 가이드를 참고하세요.

양자화 모델 서빙

Qwen3에는 FP8과 AWQ 두 가지 유형의 사전 양자화 모델이 제공돼요. 이 모델들을 서빙하는 명령은 이름만 바뀌고 원본 모델과 동일해요:

from transformers import pipeline

model_name_or_path = "Qwen/Qwen3-8B-FP8" # FP8 models
# model_name_or_path = "Qwen/Qwen3-8B-AWQ" # AWQ models

generator = pipeline(
    "text-generation",
    model_name_or_path,
    torch_dtype="auto",
    device_map="auto",
)

📝 참고: FP8 연산은 컴퓨트 능력이 8.9보다 큰 NVIDIA GPU, 즉 Ada Lovelace, Hopper 이후 GPU에서 지원돼요. 더 나은 성능을 위해 triton과 여러분 환경의 torch CUDA 버전과 호환되는 CUDA 컴파일러가 설치되어 있는지 확인하세요.

⚠️ 중요: 4.51.0 기준으로, Transformers는 GPU 간에 이들 체크포인트를 실행할 때 문제가 있어요. 다음 방법으로 문제를 우회할 수 있어요:

  • 스크립트 실행 전에 환경 변수 CUDA_LAUNCH_BLOCKING=1을 설정하거나,
  • 로컬 설치의 transformers에서 해당 줄의 주석을 해제하세요.

긴 컨텍스트 활성화

Qwen3 모델의 사전 훈련 최대 컨텍스트 길이는 32,768 토큰이에요. RoPE 스케일링 기법으로 131,072 토큰까지 확장할 수 있어요. YaRN으로 성능을 검증했어요.

Transformers는 YaRN을 지원하며, 모델 파일을 수정하거나 모델 로딩 시 기본 인자를 덮어써서 활성화할 수 있어요.

모델 파일 수정 — config.json 파일에 rope_scaling 필드를 추가하세요:

{
    ...,
    "max_position_embeddings": 131072,
    "rope_scaling": {
        "rope_type": "yarn",
        "factor": 4.0,
        "original_max_position_embeddings": 32768
    }
}

기본 인자 덮어쓰기:

from transformers import pipeline

model_name_or_path = "Qwen/Qwen3-8B"

generator = pipeline(
    "text-generation",
    model_name_or_path,
    torch_dtype="auto",
    device_map="auto",
    model_kwargs={
        "max_position_embeddings": 131072,
        "rope_scaling": {
            "rope_type": "yarn",
            "factor": 4.0,
            "original_max_position_embeddings": 32768,
        },
    }
)

⚠️ 주의: Transformers 4.52.3부터는 지정된 rope_scaling.factor와 무관하게 max_position_embeddings/rope_scaling.original_max_position_embeddings를 rope_scaling.factor로 사용해요. 자세한 내용은 이 이슈를 참고하세요.

📝 참고: Transformers는 정적 YaRN을 구현하므로 스케일링 인자가 입력 길이와 무관하게 일정하며, 이는 짧은 텍스트의 성능에 영향을 줄 수 있어요. 긴 컨텍스트 처리가 필요한 경우에만 rope_scaling 설정을 추가하는 것을 권장해요. 필요에 따라 factor를 수정하는 것도 권장해요. 예를 들어 애플리케이션의 일반적인 컨텍스트 길이가 65,536 토큰이라면 factor를 2.0으로 설정하는 것이 좋아요.

스트리밍 생성

TextStreamer를 이용하면 Qwen3와의 대화를 스트리밍 모드로 바꿀 수 있어요. 응답이 생성되는 대로 콘솔이나 터미널에 출력돼요.

from transformers import pipeline, TextStreamer

model_name_or_path = "Qwen/Qwen3-8B"

generator = pipeline(
    "text-generation",
    model_name_or_path,
    torch_dtype="auto",
    device_map="auto",
)

streamer = TextStreamer(pipe.tokenizer, skip_prompt=True, skip_special_tokens=True)

messages= generator(messages, max_new_tokens=32768, streamer=streamer)[0]["generated_text"]

TextStreamer 외에도 TextIteratorStreamer를 사용할 수 있어요. 이는 출력 준비된 텍스트를 큐에 저장해서 다운스트림 애플리케이션이 iterator로 사용할 수 있게 해줘요.

from transformers import pipeline, TextIteratorStreamer

model_name_or_path = "Qwen/Qwen3-8B"

generator = pipeline(
    "text-generation",
    model_name_or_path,
    torch_dtype="auto",
    device_map="auto",
)

streamer = TextIteratorStreamer(pipe.tokenizer, skip_prompt=True, skip_special_tokens=True)

# Use Thread to run generation in background
# Otherwise, the process is blocked until generation is complete
# and no streaming effect can be observed.
from threading import Thread
generation_kwargs = dict(text_inputs=messages, max_new_tokens=32768, streamer=streamer)
thread = Thread(target=pipe, kwargs=generation_kwargs)
thread.start()

generated_text = ""
for new_text in streamer:
    generated_text += new_text
    print(generated_text)

배치 생성

📝 참고: 배칭이 항상 성능에 유리한 것은 아니에요.

from transformers import pipeline

model_name_or_path = "Qwen/Qwen3-8B"

generator = pipeline(
    "text-generation",
    model_name_or_path,
    torch_dtype="auto",
    device_map="auto",
)
generator.tokenizer.padding_side="left"

batch = [
    [{"role": "user", "content": "Give me a short introduction to large language models."}],
    [{"role": "user", "content": "Give me a detailed introduction to large language models."}],
]

results = generator(batch, max_new_tokens=32768, batch_size=2)
batch = [result[0]["generated_text"] for result in results]

FAQ

Transformers로 분산 추론이 상상했던 것만큼 빠르지 않다는 것을 알 수 있어요. device_map="auto"를 쓴 Transformers는 텐서 병렬을 적용하지 않아 한 번에 하나의 GPU만 사용해요. 텐서 병렬을 지원하는 Transformers는 해당 문서를 참고하세요.

더 알아보기 (Learn more)