AI

Qwen3.6 서빙, 생각 모드 켜두면 GPU가 죽는다? 비용 절감의 역설

lifehardmode 2026. 5. 29. 12:08

대표 이미지이미지: lifehardmode 자체 제작

Qwen3.6 vLLM 서빙, thinking 모드 비활성화 설정과 운영 가이드

LLM을 실서비스에 올리면 사유 추론(thinking mode)이 골칫거리가 된다. 답의 정확도는 올라가지만 응답이 느려지고 GPU 메모리도 더 먹는다. Qwen/Qwen3.6-35B-A3B 같은 하이브리드 모델을 vLLM으로 띄울 때는 이 추론 단계를 기본적으로 꺼두는 쪽이 비용과 속도 면에서 유리하다. 이 글은 Docker GPU 런타임에서 chat_template_kwargs로 thinking 모드를 끄고, 여기에 LiteLLM 라우팅과 Cloudflare Tunnel을 붙여 운영하는 방식을 정리한 것이다.

thinking 모드 끄기의 기술적 근거

대상 모델은 Hugging Face 모델 카드 Qwen/Qwen3.6-35B-A3B다. 모델 카드를 보면 vLLM/SGLang OAI API에서 thinking mode를 켤지 말지를 파라미터로 넘기라고 안내한다. 실제로 로컬 vLLM 런타임에서 chat_template_kwargs={"enable_thinking": false}를 넣어 요청했더니 정상 처리됐고, 응답에 <think> 태그도 붙지 않았다.

{
  "model": "local",
  "messages": [{"role": "user", "content": "요약해줘"}],
  "chat_template_kwargs": {"enable_thinking": false}
}

요청마다 넣지 않고 서버 기본값으로 박아두려면, vLLM serve의 --default-chat-template-kwargs '{"enable_thinking": false}' 옵션을 배포 스크립트에 남겨두면 된다. 예시에서 모델명은 임의로 바꾸지 말고, 로컬 vLLM의 서빙 alias인 local과 원본 모델 카드 Qwen/Qwen3.6-35B-A3B를 함께 적어두는 게 좋다.

Docker GPU 런타임과 vLLM 시작 옵션

Docker에서 Qwen3.6을 올릴 때는 모델 옵션보다 GPU 런타임이 먼저 맞아야 한다. NVIDIA Container Toolkit이 설치돼 있는지, 컨테이너가 GPU를 보는지를 docker run --gpus all ... nvidia-smi로 먼저 확인한다. 그게 되면 vLLM 컨테이너 시작 명령에 모델 ID, API key, 포트, GPU 메모리 상한, thinking 기본값을 한꺼번에 적어둔다.

docker run --gpus all --rm   -p 127.0.0.1:8000:8000   -e VLLM_API_KEY="$VLLM_API_KEY"   vllm/vllm-openai:latest   --model Qwen/Qwen3.6-35B-A3B   --served-model-name local   --api-key "$VLLM_API_KEY"   --gpu-memory-utilization 0.90   --default-chat-template-kwargs '{"enable_thinking": false}'

--gpu-memory-utilization은 크게 잡을수록 초기 적재 여유가 늘지만, 그만큼 KV cache와 다른 프로세스 몫이 줄어든다. 두 번째 모델을 같은 GPU에 올릴 생각이라면 별도 프로세스, 포트, VRAM 상주 비용을 미리 문서화해 두자. 모델 하나만 쓸 거라면 LiteLLM을 억지로 끼울 필요 없이, vLLM 원본 포트를 loopback에 묶고 필요한 인증 계층만 앞에 두면 된다.

LiteLLM 키 관리와 라우팅

LiteLLM을 프록시로 쓸 때 API 키는 os.environ/ENV_NAME 형식으로 넘긴다. 키를 코드에 박지 않고 환경별로 분리해서 관리하기 위해서다.

config.yaml은 이런 식이다.

model_list:
  - model_name: local
    litellm_params:
      model: openai/Qwen/Qwen3.6-35B-A3B
      api_base: http://host.docker.internal:8000/v1
      api_key: os.environ/VLLM_API_KEY

여기서 local은 로컬 vLLM 인스턴스를 가리키는 alias이지 오류가 아니다. 클라이언트가 local이라는 모델명으로 호출하면 LiteLLM이 실제 엔드포인트로 라우팅한다. 키는 환경 변수로만 주입하고, 하드코딩은 하지 않는다.

Cloudflare Tunnel과 보안 계층

내부망에 띄운 vLLM을 밖으로 노출할 때는 포트 포워딩을 직접 여는 것보다 Cloudflare Tunnel 쪽이 낫다. 내부 서비스 IP를 가려주고 DDoS 방어, WAF, Access 정책 기반 인증을 얹을 수 있다.

한국 기업 환경이라면 사내 VPN이나 VPC Peering도 대안이지만, 원격 개발팀 협업이 잦다면 Tunnel이 더 유연하다. 어느 쪽이든 핵심은 Tunnel 앞단에 OAuth나 SSO 같은 인증 계층을 둬서, 인증 안 된 요청이 vLLM 인스턴스까지 닿지 못하게 막는 것이다.

한국 팀에서 달라지는 네트워크 판단

시나리오는 크게 셋으로 갈린다. 개발자가 사내에서만 쓰는 자동화라면 vLLM은 loopback이나 private subnet에 두고 OpenClaw gateway만 내부에서 접근하게 한다. 협력사나 외부 앱이 호출해야 한다면 LiteLLM이나 별도 API gateway를 인증 계층으로 세우고, Cloudflare Tunnel은 그 앞단만 노출한다. 프로덕션 내부 트래픽이라면 사내 VPN, VPC Peering, 전용선처럼 감사 로그와 네트워크 경로를 통제하기 쉬운 방식을 먼저 검토한다.

Cloudflare Tunnel은 접근 경로를 빠르게 뚫어주지만 망분리 정책까지 알아서 만족시켜 주지는 않는다. 그래서 공개 호스트, 내부 호스트, 인증 주체, 보안그룹 규칙, Access 로그, VPC Flow Logs 보관 위치는 운영 문서에 따로 남겨둬야 한다.

장애는 GPU, API, 프록시를 나눠서 본다

증상이 떴을 때 한 덩어리로 보면 원인을 못 찾는다. 층을 갈라서 본다.

첫 확인은 GPU다. nvidia-smi로 VRAM 점유, util, OOM 흔적을 본다. 로그에 CUDA out of memory가 찍혔다면 gpu-memory-utilization을 너무 높게 잡은 건 아닌지, thinking mode가 켜져서 메모리를 더 먹은 건 아닌지부터 확인하고 할당량을 조정한다.

둘째는 vLLM API다. 응답이 느리거나 <think>가 섞여 나오면, 우선 요청 바디에 chat_template_kwargs={"enable_thinking": false}가 들어갔는지 본다. 서버 기본값에 의존했다면 컨테이너 실행 명령이나 compose 파일에 --default-chat-template-kwargs가 남아 있는지 확인한다.

응답에 <think>가 그대로 남는다면 vLLM 버전과 모델 카드 간 호환성을 의심하고, 최신 vLLM 릴리스 노트에서 해당 모델 지원 여부를 다시 본다. 모델이 아예 안 뜰 땐 docker logs <container>에서 CUDA 초기화 실패, 모델 다운로드 실패, OOM kill 흔적을 찾는다.

운영 확인은 외부 공개 주소가 아니라 내부 주소에서 한다. /v1/models가 인증키로 열리는지, 반환되는 모델 ID가 local인지 본다. metrics를 켜뒀다면 /metrics에서 대기열과 토큰 처리량을 확인하고, 닫혀 있다면 서버 시작 옵션과 배포 문서를 먼저 본다.

curl -H "Authorization: Bearer $VLLM_API_KEY" http://127.0.0.1:8000/v1/models
curl http://127.0.0.1:8000/metrics
nvidia-smi

셋째는 LiteLLM이나 터널 앞단이다. 여기서는 401, 429, 5xx, upstream timeout을 갈라서 봐야 원인이 인증 문제인지 용량 문제인지 구분된다. Cloudflare Tunnel이나 LiteLLM은 내부 확인이 끝난 뒤에 붙인다. 원본 vLLM 포트를 외부에 직접 열어놓고 장애를 들여다보는 방식은 피한다.

vLLM 프로세스를 둘 띄우면 각각이 모델 가중치와 KV cache 여유분을 따로 잡는다. 그래서 --gpu-memory-utilization, 포트, 모델명, 최대 컨텍스트 길이를 프로세스별로 적어두지 않으면, 장애가 났을 때 어느 쪽이 메모리를 먹고 있는지 추적하기 어렵다.