GitHub 트렌딩 딥다이브 · 2026-06-23

KEDA GPU Scaler 딥다이브
— GPU '하드웨어 계기판'을 직접 읽어 쿠버네티스가 vLLM·추론 파드를 0개까지 자동 증감하게 만드는 오토스케일러

pmady/keda-gpu-scaler. 쿠버네티스 기본 오토스케일러(HPA)는 CPU·메모리만 볼 수 있어 GPU가 100%로 불타는데도 "CPU 8%니 한가하네"라고 착각합니다. 이 프로젝트는 GPU 노드마다 DaemonSet을 띄워 NVIDIA의 NVML 라이브러리에서 GPU 사용률·VRAM 같은 진짜 하드웨어 지표를 직접 읽고, 그것을 KEDA 외부 스케일러(gRPC) 규격으로 KEDA에 넘겨 vLLM·Triton·학습 잡을 0개까지(scale-to-zero) 자동 증감하게 합니다. 기존 정석인 dcgm-exporter → Prometheus → PromQL → KEDA(컴포넌트 5개·지연 15~30초)를 컴포넌트 2개·지연 2~4초로 줄이는 게 핵심. (저장소: pmady/keda-gpu-scaler · 언어 Go 1.25 · go-nvml(CGO) + gRPC + Prometheus · Helm 차트·CI 포함 · Apache-2.0 · TrendShift 라이브 멘션 #62133)

목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석 (GPU 온도가 파드 개수가 되기까지)
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

"방 온도계(CPU)만 보던 보일러 자동조절기에, 오븐(GPU)에 직접 꽂은 온도계를 달아 준다."

한 줄로

keda-gpu-scaler는 GPU 하드웨어 지표를 직접 읽어 KEDA에 먹여 주는 '외부 스케일러'로, 쿠버네티스가 GPU 워크로드를 0개까지 자동으로 늘리고 줄이게 만든다.

쿠버네티스의 기본 오토스케일러 HPA는 CPU·메모리만 봅니다. 그런데 LLM 추론 서버(vLLM)는 요청 200개를 처리하느라 GPU가 100%인데 CPU는 8%인 게 흔합니다. HPA 눈에는 "한가한 파드"라 절대 늘려 주지 않죠. 이 프로젝트는 GPU가 달린 노드마다 작은 감시병(DaemonSet)을 세워, NVIDIA 드라이버의 NVML에서 GPU 사용률·VRAM·온도·전력 같은 진짜 계기판 수치를 읽습니다. 그리고 그 수치를 KEDA가 알아듣는 gRPC 규격으로 넘겨, "GPU 메모리가 80% 넘으면 파드 늘려, 요청 없으면 0개로 꺼"를 자동으로 시킵니다.

용어
HPA (Horizontal Pod Autoscaler)
쿠버네티스 표준 기능으로, 부하에 따라 파드(컨테이너) 개수를 가로로 늘리고 줄이는 자동 조절기입니다. 기본적으로 CPU·메모리 사용률을 보고 판단합니다. 문제는 GPU 사용률을 못 본다는 것 — 그래서 GPU 워크로드에는 무용지물에 가깝습니다.
용어
KEDA · 외부 스케일러 (external scaler)
KEDA(Kubernetes Event-Driven Autoscaling)는 HPA의 한계를 넘어 "아무 지표로든" 오토스케일링하게 해 주는 확장 프로젝트(CNCF)입니다. 카프카 큐 길이, HTTP 요청 수, 클라우드 메트릭 등 60여 종을 기본 지원하고, 0개까지 줄이는 scale-to-zero도 됩니다. 그중 외부 스케일러는 "내가 직접 만든 gRPC 서버가 지표를 알려 줄게"라고 KEDA에 끼워 넣는 플러그인 방식 — 이 프로젝트가 바로 그 gRPC 서버입니다.

좀 더 구체적으로, keda-gpu-scaler는 Go로 만든 두 개의 작은 실행파일입니다. ① keda-gpu-scaler는 GPU 노드에서 도는 gRPC 서버(KEDA가 여기에 "지금 GPU 얼마나 써?"라고 물음), ② gpu-metrics는 쿠버네티스 없이도 터미널에서 GPU 지표를 보는 CLI(SLURM·Flux·맨몸 서버까지 같은 형식으로 출력). 핵심 자랑은 "dcgm-exporter + Prometheus 파이프라인(5개 컴포넌트, 15~30초 지연)을 컴포넌트 2개·2~4초 지연으로 압축"한다는 것 — 즉 이 프로젝트의 진짜 상품은 '단순함과 짧은 지연'입니다.

이 문서가 파고드는 건 사용법만이 아니라 그 안의 설계입니다 — "왜 GPU 지원을 KEDA 본체에 못 넣고 별도 컴포넌트로 뺐나(CGO·하드웨어 접근)", "왜 Deployment가 아니라 DaemonSet인가", "KEDA 외부 스케일러 gRPC 규격(4개 함수)은 정확히 무엇을 약속하나", "scale-to-zero는 코드로 어떻게 구현되나". 쿠버네티스 오토스케일링·gRPC·GPU 시스템 프로그래밍을 처음 공부하는 사람에게 아담하지만 알찬 실전 교본입니다.

2왜 주목받는가

"GPU는 시간당 수천 원짜리 자원이다. '안 쓸 때 0개로 끄기'를 빠르고 단순하게 해 주는 게 곧 돈이다."

먼저, GPU 오토스케일링이 왜 그렇게 어려운가

vLLM·Triton 같은 GPU 추론 서버를 쿠버네티스에 올리면, 부하가 몰릴 때 파드를 늘리고 한가하면 줄이고 싶습니다. 그런데 HPA는 GPU 사용률을 볼 줄 모릅니다. 요청 200개를 처리하느라 GPU가 포화 상태여도 CPU는 8%라, HPA는 "널널하네"라며 파드를 안 늘립니다. 반대로 요청이 끊겨도 GPU 파드는 계속 켜져 있어 시간당 수천 원짜리 GPU가 놀면서 돈만 태웁니다.

함정
정석 파이프라인의 '5개 컴포넌트, 15~30초 지연'

지금까지의 표준 해법은 dcgm-exporter(GPU 지표 수집기)가 지표를 뽑고 → Prometheus가 주기적으로 긁어가고(scrape) → PromQL 쿼리로 가공하고 → 그걸 KEDA Prometheus 스케일러가 읽는 4~5단 사슬입니다. 컴포넌트가 5개나 되고, Prometheus의 scrape 주기(보통 15초) 때문에 GPU가 달아오른 뒤 KEDA가 알아채기까지 15~30초가 걸립니다. 갑작스러운 트래픽 폭주에는 너무 느립니다.

해결
하드웨어를 직접 읽어 gRPC로 바로 먹인다 — 2개 컴포넌트, 2~4초

keda-gpu-scaler는 GPU 노드에서 NVML을 직접(2초 주기) 폴링해 메모리에 캐시하고, KEDA가 물으면 gRPC로 즉답합니다. 중간의 Prometheus·PromQL·exporter가 통째로 사라져 컴포넌트는 2개(이 스케일러 + KEDA), 지연은 2~4초. PromQL을 쓸 필요도 없이 3줄짜리 ScaledObject로 끝납니다.

"왜 KEDA 본체에 GPU 지원을 안 넣었나" — 이게 설계의 핵심

이 질문이 프로젝트 전체를 이해하는 열쇠입니다. 개발자는 세 가지 '구조적 제약' 때문에 GPU 지원을 KEDA 코어에 넣는 게 불가능하다고 설명합니다(KEDA 이슈 #7538에서 논의됨).

제약내용
① CGO 충돌NVIDIA의 Go 바인딩(go-nvml)은 C 라이브러리를 부르므로 CGO_ENABLED=1이 필수. 그런데 KEDA는 이식성을 위해 CGO_ENABLED=0으로 정적 빌드한다. 둘은 근본적으로 양립 불가.
② 노드 하드웨어 접근NVML은 /dev/nvidiactl 같은 물리 GPU 노드의 장치 파일을 통해서만 읽을 수 있다. KEDA 오퍼레이터는 중앙에 1개 떠 있는 Deployment라 워커 노드의 GPU에 손댈 수 없다 → 각 GPU 노드에 도는 DaemonSet만이 정답.
③ 독립 릴리스GPU 생태계는 빠르게 변한다. KEDA의 60여 스케일러 릴리스 주기에 묶이면 개선이 느려진다. 별도 컴포넌트면 몇 시간 만에 새 GPU 지표를 추가할 수 있다.
비유

KEDA 본체 = 본사 중앙 관제실. 회사 전체를 한 곳에서 감시하지만, 멀리 떨어진 공장 기계 내부 온도(GPU)는 직접 못 잽니다. 게다가 본사 컴퓨터에는 공장용 특수 계측 장비 드라이버(CGO/NVML)를 깔 수도 없습니다.

keda-gpu-scaler = 공장마다 배치된 계측 담당자(DaemonSet). 기계에 직접 센서를 꽂아 수치를 읽고, 관제실(KEDA)이 무전(gRPC)으로 물으면 즉답합니다. 담당자를 본사와 따로 교육·교체할 수 있어(독립 릴리스) 유연합니다.

셀링 포인트 요약

정리하면 이 프로젝트가 주목받는 이유는 네 가지입니다. ① 단순함(Prometheus 파이프라인 제거, 3줄 ScaledObject), ② 짧은 지연(직접 NVML로 2~4초), ③ scale-to-zero(요청 없으면 GPU 파드 0개 → 비용 0), ④ 워크로드별 프로필(vllm-inference·triton-inference·training 등 검증된 기본값 5종 + 멀티 GPU 집계 max/avg/min/sum). 거기에 HPC 보너스로, 같은 코드의 gpu-metrics CLI가 쿠버네티스·SLURM·Flux·맨몸 서버에서 동일한 JSON 형식으로 GPU 지표를 뽑아 온프렘과 클라우드를 같은 도구로 비교하게 해 줍니다.

함정
아직 v0.x 초기 프로젝트라는 점은 감안할 것

이 레포는 버전 v0.1~v0.4대의 신생 프로젝트이고 사실상 1인 메인테이너 중심입니다. README의 "2~4초", "30일간 무중단", "94% 토큰" 같은 수치 일부는 개발자가 4노드 A100 클러스터에서 측정·관찰한 값으로, 독립 재현 결과가 아닙니다. 또 GPU 지표 기반 스케일링은 지표가 튀면 파드도 출렁(flapping)일 수 있어, 실제로는 cooldown·activation 임계값 튜닝이 필요합니다. 개념과 코드를 배우기엔 훌륭하지만, 프로덕션 도입 전엔 충분한 부하 테스트가 권장됩니다.

3기술 스택 전체 지도

"전부 Go 한 덩어리지만, 진짜 재미는 'Go가 C(NVML)와 손잡는 지점'과 'gRPC로 KEDA와 약속을 맺는 지점'에 있다."

keda-gpu-scaler는 화면(UI)이 없는 순수 백엔드/인프라 도구입니다. 무게중심은 "GPU에서 숫자를 읽어(NVML) → 약속된 규격으로 내보낸다(gRPC)"의 두 다리에 쏠려 있고, 나머지는 관측성·배포·테스트를 위한 견고한 살입니다.

① 코어 (GPU 읽기 + 스케일러 두뇌)

기술역할한 줄 설명
Go 1.25전체 언어단일 바이너리·고루틴(동시성)·빠른 시작. 폴링 루프와 gRPC 서버를 가볍게 돌린다.
NVIDIA/go-nvml
(CGO)
GPU 지표 수집NVIDIA의 libnvidia-ml.so(NVML) C 라이브러리를 Go에서 호출. 사용률·VRAM·온도·전력·PCIe·NVLink를 읽는다. CGO 필수.
google.golang.org/grpc
+ protobuf
KEDA 인터페이스KEDA 외부 스케일러 규격(ExternalScaler 서비스)을 protobuf로 정의·구현. 타입 안전 + 스트리밍 지원.
go.uber.org/zap구조적 로깅JSON 형식 로그(필드 기반). 운영 환경에서 검색·집계하기 좋다.
용어
NVML (NVIDIA Management Library)
NVIDIA 드라이버가 제공하는 GPU 상태 조회용 C 라이브러리(libnvidia-ml.so). nvidia-smi 명령이 안에서 쓰는 바로 그 라이브러리입니다. GPU 사용률·메모리·온도·전력·PCIe/NVLink 대역폭을 읽을 수 있고, 읽기 전용 호출이라 장치 설정을 바꾸지 않습니다. C라서 Go에서 쓰려면 go-nvml 바인딩 + CGO 다리가 필요합니다.
용어
gRPC · protobuf
gRPC는 구글이 만든 고성능 RPC(원격 함수 호출) 프레임워크로, 함수의 입출력 모양을 protobuf(.proto 파일)로 미리 약속하면 코드를 자동 생성해 줍니다. KEDA는 외부 스케일러와 대화할 때 이 gRPC 규격을 쓰기로 정해 뒀습니다. HTTP로 텍스트를 주고받는 것보다 타입이 안전하고(PromQL 문자열 파싱 불필요) 빠르며 스트리밍이 됩니다.

② 관측성 · 헬스 (스케일링과 '분리된' 모니터링 경로)

기술역할설명
prometheus/client_golang선택적 /metricsGPU 사용률·메모리·온도·전력 + 스케일러 자체 통계(NVML 호출 수·에러·지연 히스토그램)를 :9090/metrics로 노출. 스케일링 경로와 독립 — 꺼도(--metrics-port=0) 스케일링은 그대로.
HTTP 헬스 프로브liveness/readiness/healthz(살아있음, 200)·/readyz(NVML 초기화 + 첫 수집 성공 후 200)를 :8081에 노출. 쿠버네티스가 파드 상태를 판단.
gRPC health + reflection디버깅표준 gRPC 헬스체크와 reflection(서비스 목록 조회) 등록 → grpcurl로 직접 찔러볼 수 있다.

③ 인프라 · 빌드 · 배포

항목내용
Dockerfile2단계 빌드: ① golang:1.26-bookworm에서 gcc로 CGO_ENABLED=1 빌드 → ② 런타임은 nvidia/cuda:13.3.0-base(libnvidia-ml.so 포함) 위에 바이너리만 복사. 비루트 UID(65534)로 실행.
Helm 차트deploy/helm/에 DaemonSet·Service·ServiceAccount 템플릿 + values.yaml(nodeSelector·tolerations·privileged·metrics/probes 토글·nvidia runtimeClass 등).
생(raw) 매니페스트deploy/manifests.yaml — Helm 없이 kubectl apply 한 방으로 Service + DaemonSet 배포.
예제deploy/examples/ — vLLM용 ScaledObject, 커스텀 GPU 사용률 ScaledObject.
품질·CIGitHub Actions(ci·codeql·scorecard·release), golangci-lint, Go Report Card 배지, mkdocs(ReadTheDocs) 문서 사이트 — 오픈소스 위생이 잘 갖춰짐.
Makefilemake build / build-metrics / test / test-e2e / lint / proto / docker-release / deploy / helm-test 한 줄 명령 모음.

④ 보너스 — 크로스 환경 CLI (gpu-metrics)

같은 코드베이스에 gpu-metrics라는 독립 CLI가 들어 있습니다. 쿠버네티스가 전혀 없어도 베어메탈·SLURM·Flux·싱귤래리티 컨테이너에서 GPU 지표를 table/json/csv로 뽑습니다. 환경을 자동 감지(--env auto)하고, 어디서 돌든 동일한 JSON 스키마(environment 블록 포함)로 출력해 온프렘과 클라우드 GPU 성능을 jq·pandas로 손쉽게 비교하게 합니다.

4아키텍처 심화 분석 (GPU 온도가 파드 개수가 되기까지)

"GPU 하드웨어 → NVML → DaemonSet(2초 폴링) → gRPC → KEDA → HPA → 파드 개수. 한 줄로 흐른다."

전체 데이터 흐름 한 장

GPU 노드 (워커) KEDA 오퍼레이터 (중앙) ┌─────────────────────────────┐ ┌────────────────────────┐ │ DaemonSet: keda-gpu-scaler │ │ │ │ │ gRPC :6000 │ ExternalScaler 트리거 │ │ ┌───────────────┐ │ GetMetrics │ (ScaledObject 설정) │ │ │ NVML 폴러 │──캐시──▶ │ ◀───────────── │ │ │ │ (2초 루프) │ 메모리 │ ──── 응답 ───▶ │ → HPA 결정 │ │ └───────────────┘ │ │ → 파드 N개로 증감 │ │ ▲ │ └───────────┬────────────┘ │ libnvidia-ml.so (NVML) │ ▼ │ /dev/nvidiactl, /dev/... │ ┌────────────────────┐ └─────────────────────────────┘ │ 대상 Deployment │ │ (선택) :9090/metrics → Prometheus │ (vLLM/Triton/학습) │ │ :8081 /healthz /readyz │ replicas: 0 ↔ 50 │ ▼ └────────────────────┘ GPU 플릿 모니터링(스케일링과 독립)

흐름은 일곱 단계입니다. DaemonSet이 NVML 폴링 루프를 시작(기본 2초) → 매 주기 SM 사용률·메모리·VRAM·온도·전력을 읽어 메모리에 캐시(디스크·외부 저장소 없음) → KEDA가 gRPC로 GetMetrics() 호출 → 스케일러가 ScaledObject에 적힌 집계 방식대로 값을 계산해 응답 → KEDA가 그 값을 HPA에 먹임 → HPA가 파드를 늘리거나 줄이거나(0개 포함) 결정. 선택적으로 :9090/metrics가 Prometheus용 게이지를 노출하지만 이건 스케일링 경로와 완전히 분리돼 있어 꺼도 무방합니다.

설계 패턴 ① — KEDA 외부 스케일러 gRPC 규격(4개 함수)

이 프로젝트의 심장은 proto/externalscaler.proto에 정의된 KEDA와의 약속입니다. KEDA가 부를 수 있는 함수는 정확히 네 개입니다.

service ExternalScaler {
    rpc IsActive(ScaledObjectRef) returns (IsActiveResponse) {}
    rpc StreamIsActive(ScaledObjectRef) returns (stream IsActiveResponse) {}
    rpc GetMetricSpec(ScaledObjectRef) returns (GetMetricSpecResponse) {}
    rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse) {}
}
// 메타데이터는 map<string, string> 으로 전달 (ScaledObject에 쓴 설정 그대로)
함수역할 (코드 기준)
IsActivescale-to-zero의 핵심. 현재 GPU 지표가 activationThreshold를 넘으면 true. 0개로 꺼진 상태에서 true가 되면 KEDA가 파드를 0→1로 깨운다. 반대로 한가하면 false → 0개로 잠재움.
StreamIsActive위를 스트리밍으로. 폴링 주기(ticker)마다 활성 여부를 push. 끌어오기(pull) 대신 밀어주기(push) 방식.
GetMetricSpecHPA에게 "이 지표 이름은 X, 목표값은 Y"라고 알려 준다(metricName + targetSizeFloat).
GetMetrics지금 이 순간의 GPU 지표 실제 값을 돌려준다. HPA가 "목표 대비 현재"를 비교해 파드 수를 계산.
비유

온도조절기(HPA)와 보일러 센서(스케일러)의 대화로 보면: GetMetricSpec은 "목표 온도는 23도야"라고 합의하는 단계, GetMetrics는 "지금 21도"라고 현재값을 보고하는 단계, IsActive는 "사람이 방에 들어왔나?(켤 가치가 있나)"를 묻는 단계입니다. 아무도 없으면(IsActive=false) 보일러를 완전히 꺼 버리는 게 scale-to-zero입니다.

설계 패턴 ② — Deployment가 아니라 DaemonSet인 이유

NVML은 /dev/nvidiactl물리 노드의 장치 파일을 통해서만 GPU를 읽습니다. 중앙에 1개 뜨는 Deployment로는 다른 노드의 GPU를 볼 수 없죠. 그래서 모든 GPU 노드에 1개씩 떠야 하는 DaemonSet이 유일한 정답입니다. 매니페스트의 핵심 설정을 보면 의도가 또렷합니다.

# GPU 있는 노드에만 스케줄
nodeSelector:
  nvidia.com/gpu.present: "true"
# GPU 노드의 taint를 견뎌 쫓겨나지 않게
tolerations:
  - key: "nvidia.com/gpu"
    operator: "Exists"
    effect: "NoSchedule"
# NVML이 장치 파일에 접근하려면 권한 + hostPath 마운트 필요
securityContext:
  privileged: true
volumeMounts:
  - { name: dev-nvidiactl,  mountPath: /dev/nvidiactl }
  - { name: nvidia-driver,  mountPath: /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1 }
용어
DaemonSet · nodeSelector · toleration · hostPath
DaemonSet은 "해당하는 모든 노드에 파드를 정확히 1개씩 띄우는" 쿠버네티스 워크로드 — 로그 수집기·노드 모니터에 흔히 씁니다. nodeSelector는 "이 라벨 붙은 노드에만 띄워"(여기선 GPU 노드), toleration은 "이 노드의 출입 금지 표식(taint)을 나는 견딘다"는 허가증, hostPath는 "호스트(노드)의 파일/장치를 컨테이너 안으로 마운트"하는 통로입니다. GPU 장치 파일과 NVML 라이브러리를 컨테이너로 끌어오려고 씁니다.

설계 패턴 ③ — CGO 다리와 '인터페이스로 GPU 갈아끼우기'

GPU 읽기는 C(NVML)에 의존하지만, 코드 대부분은 인터페이스 뒤에 숨겨져 있습니다. 핵심은 이 작은 약속입니다.

type MetricsCollector interface {
    CollectAll() ([]Metrics, error)        // 모든 GPU
    CollectDevice(index int) (Metrics, error) // 특정 GPU
    DeviceCount() (int, error)
    Close() error
}

실제 구현(nvml.go)은 NVML을 호출하지만, 테스트용 mock 구현(mock.go)도 같은 인터페이스를 만족합니다. 덕분에 GPU가 없는 노트북·CI에서도 단위/E2E 테스트가 전부 돌아갑니다 — 스케일러 두뇌(gRPC·집계·프로필)는 GPU 없이 검증하고, 진짜 하드웨어는 클러스터에서만 확인. 이것이 의존성 주입(DI)과 인터페이스 설계의 교과서적 활용입니다.

함정
"빌드는 됐는데 파드가 바로 죽어요"

바이너리는 libnvidia-ml.so런타임에 동적 로드합니다. NVIDIA 드라이버가 없는 머신(노트북·일반 CI 러너)에서는 nvml init failed로 즉시 종료됩니다. 빌드도 CGO_ENABLED=1 + C 컴파일러가 없으면 실패합니다. "GPU 없이 되는 건 테스트(mock)뿐"이라는 걸 기억하세요.

설계 패턴 ④ — 프로필 + 메타데이터로 설정 해석

사용자가 "raw 지표 임계값"을 직접 정하는 건 실수하기 쉽습니다("vLLM에 GPU 80%가 맞나?"). 그래서 워크로드별 프로필을 미리 만들어 뒀습니다. 스케일러는 ScaledObject의 메타데이터를 parseMetadata()로 읽어 기본값 → 프로필 → 개별 override 순서로 설정을 겹쳐 해석합니다.

프로필주 지표목표 / 활성용도
vllm-inference메모리 %80 / 5vLLM·LLM 서빙(KV 캐시 선점형), scale-to-zero
triton-inferenceGPU 사용률75 / 10Triton 다중 모델 서빙(플래핑 방지로 활성 10)
trainingGPU 사용률90 / 0학습 잡(체크포인트 보호 위해 0개로 안 끔)
batch메모리 %70 / 1오프라인 배치 추론(공격적 scale-down)
distributed-trainingNVLink TX800 / 100NVLink 기반 데이터 병렬 학습

멀티 GPU 노드(4~8장)에선 GPU별 값을 하나로 합쳐야 하는데, aggregation으로 max(아무 GPU나 뜨거우면 증설 — 추론 기본값)·avg(평균 — 학습)·min(보수적)·sum(총량 기반)을 고릅니다.

용어
scale-to-zero · activation threshold (활성 임계값)
scale-to-zero는 일이 없을 때 파드를 0개까지 줄이는 것 — GPU처럼 비싼 자원에선 비용을 0으로 만드는 결정적 기능입니다. 다만 0개일 땐 지표를 볼 파드 자체가 없으므로, 별도 신호로 "다시 켤지"를 판단해야 합니다. 그 신호가 activation threshold입니다. IsActive가 "지표 > 활성임계값"이면 true를 반환해 0→1로 깨웁니다. 목표값(target)이 '몇 개로 늘릴지'라면, 활성값은 '아예 켤지 말지'의 스위치입니다.

설계 패턴 ⑤ — 크로스 환경 통합(pkg/env): 어디서든 같은 JSON

gpu-metrics CLI는 HPC와 클라우드를 동등하게 노립니다. env.Detect()가 환경변수를 SLURM → Flux → 쿠버네티스 → 맨몸 우선순위로 검사해 자동 판별하고(예: SLURM_JOB_ID 있으면 SLURM), 스케줄러가 배정한 GPU만 골라 수집합니다. 출력은 어디서 돌든 항상 같은 스키마라 파서가 환경별로 분기할 필요가 없습니다.

{ "environment": { "orchestrator": "slurm", "node": "compute-01",
                    "job_id": "123", "task_rank": 0 },
  "collected_at": "2026-06-17T10:00:00Z",
  "devices": [ ... ] }

이 "통일된 어휘" 설계 덕에 온프렘 SLURM 클러스터와 클라우드 쿠버네티스의 GPU 성능을 동일한 도구·동일한 JSON으로 비교할 수 있습니다.

5디렉토리 구조 해부

"표준 Go 레이아웃(cmd · pkg). 작지만 '실행파일 2개 · 패키지마다 _test.go' 로 군더더기 없이 짜여 있다."

최상위 한눈에

keda-gpu-scaler/ ├── cmd/ │ ├── keda-gpu-scaler/main.go ← gRPC 스케일러 서버(진입점). 플래그·헬스·프로메테우스 조립 │ └── gpu-metrics/main.go ← 독립 GPU 지표 CLI(table/json/csv, 크로스 환경) ├── pkg/ │ ├── scaler/ · KEDA gRPC 4개 함수 구현 + 메타데이터 파싱 + 멀티GPU 집계 │ ├── gpu/ · MetricsCollector 인터페이스 + nvml.go(실제) + mock.go(테스트) │ ├── profiles/ · 워크로드 프로필 5종 + 지표 타입 상수 │ ├── env/ · 환경 자동감지(k8s/slurm/flux/standalone) + 통일 Context │ ├── slurm/, flux/ · 각 스케줄러별 잡/GPU 배정 정보 파싱 │ ├── metrics/ · Prometheus 계측 래퍼(컬렉터를 감싸 호출 수·지연 기록) │ ├── probes/ · /healthz · /readyz HTTP 핸들러 │ └── externalscaler/ · proto에서 생성된 Go 코드(.pb.go) — 직접 편집 안 함 ├── proto/externalscaler.proto ← KEDA와의 gRPC 약속(원본) ├── deploy/ │ ├── manifests.yaml · Service + DaemonSet (kubectl apply 한 방) │ ├── helm/keda-gpu-scaler/ · Helm 차트(values.yaml + 템플릿) │ └── examples/ · vLLM·커스텀 ScaledObject 예제 ├── docs/ ← mkdocs 문서(DESIGN/FAQ/MIGRATION/HPC/getting-started…) ├── tests/e2e/ ← mock 컬렉터로 gRPC 전 경로 E2E 테스트(GPU 불필요) ├── Dockerfile · Makefile · go.mod · go.sum └── ARCHITECTURE.md · BENCHMARKS.md · CHANGELOG.md · ROADMAP.md · SECURITY.md
용어
cmd / pkg (Go 표준 레이아웃)
Go 커뮤니티 관습으로, cmd/는 실행파일의 main 진입점(여기선 서버와 CLI 둘), pkg/는 기능별로 나뉜 재사용 가능한 패키지입니다. 이 레포는 gortex처럼 거대하진 않지만, 패키지마다 _test.go가 짝지어 있어(env_test·collector_test·server_test…) 테스트 문화가 탄탄합니다. pkg/externalscaler/*.pb.gomake proto가 자동 생성하므로 손대지 않습니다.

기능별로 묶어 보기

묶음대표 위치하는 일
진입점cmd/keda-gpu-scaler, cmd/gpu-metrics서버 부팅(플래그·로거·gRPC·헬스·프로메테우스 와이어링) / 독립 CLI
스케일러 두뇌pkg/scalergRPC 4개 함수, ScaledObject 메타데이터 해석, 지표 추출·멀티GPU 집계
GPU 읽기pkg/gpu (nvml·mock·collector)NVML 실제 호출 + 동일 인터페이스의 테스트용 mock
설정·프로필pkg/profilesvllm/triton/training/batch/distributed-training 기본값과 지표 타입
크로스 환경pkg/env, pkg/slurm, pkg/flux오케스트레이터 감지 + 잡/GPU 배정 파싱 + 통일 JSON 컨텍스트
관측성pkg/metrics, pkg/probesPrometheus 계측 래퍼 / liveness·readiness 프로브
규격·배포proto/, deploy/(helm·manifests·examples)KEDA gRPC 계약 + DaemonSet/Service/ScaledObject 배포물
비유

레스토랑으로 보면, pkg/gpu는 재료를 직접 따 오는 텃밭(NVML), pkg/scaler는 주문을 해석해 요리하는 주방(gRPC 응답), pkg/profiles는 정해진 레시피북, deploy/는 가게를 차리는 인테리어 도면(어디에·어떻게 배치할지)입니다. mock.go는 텃밭 없이도 주방을 연습할 수 있는 '모형 재료'라, GPU 없이 주방 로직을 다 테스트하게 해 줍니다.

6학습 포인트 (기술별 — 배울 것 + 실습 아이디어)

"이 한 레포 안에 쿠버네티스 오토스케일링 · gRPC · GPU 시스템 프로그래밍 · 인터페이스 설계가 다 들어 있다."

① KEDA 외부 스케일러 & gRPC 계약

배울 것: KEDA가 "아무 지표로든" 스케일링하게 해 주는 외부 스케일러의 gRPC 4함수 계약(IsActive·StreamIsActive·GetMetricSpec·GetMetrics). 특히 scale-to-zero가 IsActive라는 별도 게이트로 구현된다는 점.

실습: externalscaler.proto를 그대로 가져와, GPU 대신 "랜덤 숫자"나 "현재 분(minute)"을 지표로 돌려주는 미니 외부 스케일러를 Go로 구현하고, ScaledObject로 KEDA에 물려 파드가 늘었다 줄었다 하는지 본다.

② 쿠버네티스 HPA / 오토스케일링 내부

배울 것: HPA가 "목표값 대비 현재값"으로 파드 수를 계산하는 원리, KEDA가 그 위에서 어떻게 동작하고 scale-to-zero·cooldown·플래핑 방지(stabilization)를 어떻게 다루는지.

실습: 로컬 kind/minikube에 KEDA를 깔고, CPU 기반 ScaledObject로 부하를 줘서 파드가 0→N→0으로 변하는 과정을 kubectl get hpa,scaledobject -w로 관찰.

③ NVML · GPU 지표 · CGO

배울 것: nvidia-smi 뒤에 있는 NVML로 GPU 사용률·VRAM·온도·전력·NVLink를 읽는 법, 그리고 Go에서 C 라이브러리를 부르는 CGO의 빌드 제약(C 컴파일러·동적 링크).

실습: GPU 머신에서 go-nvml로 디바이스 개수와 각 GPU의 사용률·메모리를 출력하는 30줄짜리 프로그램 작성. nvidia-smi 값과 비교해 단위(MiB·milliwatt) 변환을 확인.

④ DaemonSet · 노드 하드웨어 접근

배울 것: 노드 단위 하드웨어를 다루는 쿠버네티스 패턴 — DaemonSet + nodeSelector + toleration + privileged + hostPath, 그리고 파드가 자기 노드/네임스페이스를 아는 Downward API(NODE_NAME·POD_NAME 주입).

실습: 모든 노드에서 호스트의 /proc/loadavg를 읽어 로그로 찍는 간단한 DaemonSet을 작성하고, hostPath 마운트와 nodeSelector를 직접 붙여 본다.

⑤ Go에서 gRPC + protobuf 실전

배울 것: .proto → 코드 생성(protoc) → 서버 구현 → 헬스체크·reflection 등록까지의 전체 흐름. grpcurl로 살아있는 gRPC 서버를 직접 찔러 보는 디버깅법.

실습: 이 레포의 서버를 mock으로 띄우고(make test-e2e 경로 참고), grpcurl -plaintext localhost:6000 externalscaler.ExternalScaler/GetMetrics로 응답을 받아 본다.

⑥ 인터페이스 기반 설계 = 테스트 가능성

배울 것: MetricsCollector 인터페이스 하나로 실물(NVML)과 모형(mock)을 갈아끼우는 의존성 주입 패턴. "GPU 없는 CI에서도 핵심 로직을 100% 테스트"하는 비결이자, 좋은 Go 설계의 정석.

실습: 내 프로젝트의 외부 의존(파일·HTTP·DB) 한 곳을 인터페이스로 추출하고, 진짜 구현과 fake 구현을 만들어 단위 테스트를 fake로 돌려 본다.

⑦ 관측성: Prometheus 계측 래퍼 · 헬스 프로브

배울 것: 컬렉터를 감싸서(decorator) 호출 수·에러·지연을 자동 기록하는 Prometheus 계측 패턴, 그리고 /healthz(liveness)·/readyz(readiness)의 의미 차이(살아있음 vs 트래픽 받을 준비됨).

실습: 임의의 함수를 감싸 "호출 횟수 카운터 + 실행시간 히스토그램"을 기록하는 래퍼를 만들고 /metrics로 노출해 본다.

7하드웨어 / 시스템 요구사항

"NVIDIA GPU + 드라이버는 필수. 대신 스케일러 자체는 노드당 ~20Mi RAM의 깃털 같은 무게다."

항목요구사항 / 메모
쿠버네티스NVIDIA GPU 워커 노드가 있는 클러스터(OKE·GKE·EKS·AKS 등) + KEDA v2.10+ 설치.
GPU 드라이버NVIDIA 드라이버 + Device Plugin 필수. 드라이버가 libnvidia-ml.so를 제공한다. 노드에 nvidia.com/gpu.present=true 라벨 필요.
CGO (빌드)CGO_ENABLED=1 + C 컴파일러(gcc) 필요 — go-nvml이 C 라이브러리라서. 순수 Go 크로스 컴파일이 까다로운 지점.
런타임 의존바이너리가 실행 시 libnvidia-ml.so를 동적 로드. NVIDIA 드라이버 없는 머신에선 즉시 종료(nvml init failed).
권한DaemonSet은 privileged + runAsUser:0으로 장치 파일에 접근(또는 nvidia runtimeClass 사용). hostPath로 /dev/nvidiactl·/dev/nvidia-uvm·NVML 라이브러리 마운트.
리소스(스케일러)매우 가벼움 — CPU 50~200m, 메모리 ~18Mi(1 GPU)~22Mi(8 GPU). 디스크·영구저장소 0. NVML 호출은 GPU당 ~0.3ms.
포트6000 gRPC(KEDA), 9090 Prometheus(선택), 8081 헬스 프로브.
외부 서비스없음. Prometheus·dcgm-exporter 없이도 스케일링 동작(Prometheus는 선택적 모니터링용).
함정
"privileged DaemonSet"의 보안 무게

NVML 장치 접근을 위해 privileged 컨테이너를 띄우는 건 보안상 가벼운 결정이 아닙니다. 다만 이 스케일러는 읽기 전용 NVML 호출만 하고, gRPC 포트는 클러스터 내부 ClusterIP로만 노출되며, 비밀정보·외부 RBAC가 필요 없습니다. 가능하면 hostPath 직접 마운트 대신 nvidia runtimeClass로 장치 마운트를 위임하는 편이 더 깔끔합니다(values.yaml에 토글 있음).

8직접 해볼 수 있는 실습 과제

"GPU 없이 코드부터 → 클러스터에 KEDA 깔기 → ScaledObject 붙이기 → 직접 스케일러 만들기, 단계별로."

과제 1난이도 ★☆☆☆☆ · 입문

proto 읽고, GPU 없이 E2E 테스트 돌려 보기

proto/externalscaler.proto로 KEDA의 4함수 계약을 눈에 익히고, make test/make test-e2e를 실행한다(GPU 불필요 — mock 컬렉터 사용). pkg/scaler/server_test.go에서 "메타데이터 → 지표 추출 → gRPC 응답" 경로가 어떻게 검증되는지 따라간다.

목표: 하드웨어 없이도 핵심 로직이 전부 테스트된다는 걸 체감. mock이 인터페이스를 어떻게 만족시키는지 확인.

과제 2난이도 ★★☆☆☆ · 초급

로컬에 KEDA 깔고 scale-to-zero 관찰

kind/minikube에 KEDA를 설치하고, CPU 기반 ScaledObject로 부하를 줘 파드가 0 → N → 0으로 변하는 걸 본다. kubectl get scaledobject,hpa,pods -w로 실시간 관찰.

목표: "외부 스케일러"를 붙이기 전에 KEDA·HPA·scale-to-zero의 기본 동작을 몸에 익히기. cooldownPeriod를 바꿔 가며 반응 속도 비교.

과제 3난이도 ★★★☆☆ · 중급

vLLM 디플로이먼트에 프로필로 ScaledObject 붙이기

GPU 클러스터에 이 스케일러를 배포(kubectl apply -f deploy/manifests.yaml 또는 Helm)하고, deploy/examples/vllm-scaledobject.yaml처럼 profile: "vllm-inference" 한 줄로 vLLM 파드에 오토스케일링을 건다. 부하를 주며 GPU 메모리% 변화에 따라 파드가 느는지 확인.

목표: 프로필 → 실제 스케일링 동작 연결. 그다음 profile 대신 metricType: gpu_utilization + targetValue + aggregation: avg로 raw 설정도 시도해 차이를 본다.

과제 4난이도 ★★★★☆ · 중상

나만의 외부 스케일러를 처음부터 만들기

이 레포의 proto를 재사용해, GPU 대신 임의 지표(예: 외부 API의 큐 길이, 또는 현재 시각)를 돌려주는 외부 스케일러를 Go로 작성한다. cmd/keda-gpu-scaler/main.go의 gRPC·헬스·graceful shutdown 와이어링을 본보기로 삼는다.

목표: "GPU"라는 껍데기를 벗기면 결국 KEDA 외부 스케일러 패턴이 남는다는 걸 이해. 내 비즈니스 지표로 쿠버네티스를 자동 스케일하는 길을 연다.

과제 5난이도 ★★★★★ · 고급

새 지표/집계 추가 또는 MIG 대응

pkg/gpu(NVML 수집)·pkg/profiles(지표 타입)·pkg/scaler(extractMetric/aggregate)를 읽고, 새 지표(예: NVLink 합산 방식 개선, 또는 ROADMAP의 MIG per-instance 지표)나 새 집계 전략(예: p95)을 추가한다. mock으로 단위 테스트까지.

목표: 수집 → 추출 → 집계 → gRPC 응답의 데이터 경로를 끝까지 손으로 확장. NVIDIA MIG(GPU 분할)나 AMD ROCm 같은 미래 과제에 기여할 준비.

9관련 기술 심화 학습 로드맵 (6주 플랜)

"쿠버네티스 오토스케일링에서 출발해 gRPC·GPU 시스템·관측성까지, 이 레포를 길잡이 삼아."

주차주제핵심 학습 + 이 레포 연결점
1주쿠버네티스 + HPA 기초파드·디플로이먼트·서비스, HPA의 목표값 기반 스케일링 원리 → "왜 HPA는 GPU를 못 보나"
2주KEDA · ScaledObject · 스케일러이벤트 기반 오토스케일링, scale-to-zero, 트리거 종류 → deploy/examples ScaledObject 읽기
3주Go에서 gRPC + protobuf.proto → 코드생성 → 서버 구현, 헬스/reflection → proto/ + pkg/scaler
4주외부 스케일러 계약 — 직접 구현IsActive/GetMetricSpec/GetMetrics의 의미, 메타데이터 해석 → pkg/scaler/server.go
5주NVML · CGO · DaemonSet 하드웨어GPU 지표 읽기, CGO 빌드, nodeSelector/toleration/hostPath/Downward API → pkg/gpu + deploy/manifests.yaml
6주관측성 + Helm 패키징 + 배포Prometheus 계측·헬스 프로브, Helm 차트 작성, 2단계 Docker(CGO) 빌드 → pkg/metrics·probes + deploy/helm + Dockerfile
비유

이 로드맵은 "쿠버네티스 운영 → 이벤트 기반 자동화 → 분산 통신(gRPC) → 하드웨어 시스템 → 프로덕션 패키징"을 한 줄로 꿴 코스입니다. 클라우드 네이티브 인프라 엔지니어가 갖춰야 할 핵심 축을 작고 읽기 쉬운 실전 레포 하나로 훑을 수 있어, MLOps·플랫폼 엔지니어링 입문에 특히 좋습니다.

10핵심 키워드 사전

"이 문서와 저장소에서 반복되는 용어를 한곳에 모았다."

용어의미
HPA쿠버네티스 기본 오토스케일러. CPU·메모리 기준으로 파드 개수를 가로로 증감. GPU는 못 봄
KEDA이벤트/임의 지표 기반 오토스케일링 확장(CNCF). scale-to-zero 지원, 60여 스케일러 내장
외부 스케일러 (external scaler)KEDA에 gRPC 서버로 끼워 넣는 커스텀 지표 공급자. 이 프로젝트가 그 서버
ScaledObject"무엇을·어떤 지표로·몇 개까지 스케일링할지" 정의하는 KEDA 커스텀 리소스(YAML)
scale-to-zero일이 없을 때 파드를 0개까지 줄이는 것. 비싼 GPU 비용을 0으로 만드는 핵심 기능
activation threshold0개에서 다시 켤지를 정하는 임계값. IsActive가 "지표 > 활성값"이면 0→1로 깨움
NVMLNVIDIA 드라이버의 GPU 상태 조회 C 라이브러리(libnvidia-ml.so). nvidia-smi의 내부
go-nvml / CGONVML의 Go 바인딩 / Go에서 C를 부르는 다리(CGO_ENABLED=1 + C 컴파일러 필요)
gRPC / protobuf타입 안전한 고성능 RPC / 그 입출력 스키마를 정의하는 .proto. KEDA-스케일러 통신 규격
DaemonSet해당 노드마다 파드를 정확히 1개씩 띄우는 워크로드. 노드 단위 하드웨어 접근에 필수
nodeSelector / toleration"GPU 노드에만 배치" / "GPU 노드의 taint를 견딤". GPU 노드에 스케줄되게 하는 짝
privileged / hostPath장치 접근 권한 / 호스트의 장치·라이브러리를 컨테이너로 마운트하는 통로
Downward API파드가 자기 노드명·파드명·네임스페이스를 환경변수로 알게 해 주는 쿠버네티스 기능
dcgm-exporterNVIDIA의 GPU 지표 수집기. 기존 정석(→Prometheus)의 출발점. 이 프로젝트가 제거 대상으로 삼음
Prometheus / PromQL지표 수집·저장 시스템 / 그 질의어. 기존 파이프라인의 지연(scrape 15s)·복잡성의 원인
vLLM / Triton대표적 GPU 추론 서버. vLLM은 KV 캐시 선점형(메모리 기준 스케일), Triton은 다중 모델(사용률 기준)
SM 사용률GPU 연산 코어(Streaming Multiprocessor)의 사용률 %. "GPU가 얼마나 바쁜가"의 핵심 지표
VRAM / memory %GPU 비디오 메모리 사용량/비율. vLLM처럼 메모리를 선점하는 워크로드의 스케일 기준
NVLink / PCIe 처리량GPU↔GPU(NVLink)·CPU↔GPU(PCIe) 데이터 대역폭. 분산 학습 스케일 신호로 활용
aggregation멀티 GPU 노드에서 GPU별 값을 하나로 합치는 방식: max·avg·min·sum
profile워크로드별로 검증된 지표·목표·활성값 묶음(vllm/triton/training/batch/distributed-training)
MIGNVIDIA Multi-Instance GPU — 한 GPU를 여러 인스턴스로 쪼개는 기술(ROADMAP의 향후 지원 대상)
liveness / readiness 프로브/healthz(살아있음) · /readyz(NVML 초기화+첫 수집 성공해 트래픽 받을 준비됨)
SLURM / FluxHPC 클러스터의 잡 스케줄러. gpu-metrics CLI가 자동 감지해 배정된 GPU만 수집

11참고 링크