[vLLM] Observability: 추적, 프로파일링, 메트릭
들어가며
프로덕션 LLM 서빙에서 관측 가능성(Observability)은 필수다. vLLM은 vllm/tracing/에서 분산 추적을, vllm/profiler/에서 성능 프로파일링을, vllm/usage/에서 사용량 수집을 구현하고 있다. 이 글에서는 OpenTelemetry 기반 추적 시스템을 중심으로 분석한다.
공식 문서
vLLM 공식 문서: Metrics
핵심 구조/코드 분석
OpenTelemetry 트레이서 초기화
def init_otel_tracer(
instrumenting_module_name: str,
otlp_traces_endpoint: str,
extra_attributes: dict[str, str] | None = None,
) -> Tracer:
os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = otlp_traces_endpoint
resource_attrs = {}
resource_attrs["vllm.instrumenting_module_name"] = instrumenting_module_name
resource_attrs["vllm.process_id"] = str(os.getpid())
if extra_attributes:
resource_attrs.update(extra_attributes)
resource = Resource.create(resource_attrs)
trace_provider = TracerProvider(resource=resource)
span_exporter = get_span_exporter(otlp_traces_endpoint)
trace_provider.add_span_processor(BatchSpanProcessor(span_exporter))
set_tracer_provider(trace_provider)
atexit.register(trace_provider.shutdown)
return trace_provider.get_tracer(instrumenting_module_name)
OTLP 엔드포인트를 환경 변수로 전파하여, 자식 프로세스(워커)에서도 동일한 엔드포인트를 사용할 수 있다. gRPC와 HTTP/protobuf 두 가지 프로토콜을 지원한다:
def get_span_exporter(endpoint):
protocol = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, "grpc")
if protocol == "grpc":
return OTLPGrpcExporter(endpoint=endpoint, insecure=True)
elif protocol == "http/protobuf":
return OTLPHttpExporter(endpoint=endpoint)
워커 프로세스 트레이서
def init_otel_worker_tracer(instrumenting_module_name, process_kind, process_name):
otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
if not otlp_endpoint:
return None
extra_attrs = {
"vllm.process_kind": process_kind,
"vllm.process_name": process_name,
}
return init_otel_tracer(instrumenting_module_name, otlp_endpoint, extra_attrs)
각 워커 프로세스가 자체 트레이서를 초기화한다. process_kind(예: "gpu_worker")와 process_name으로 스팬을 구분할 수 있다.
프로세스 간 트레이스 전파
@contextmanager
def propagate_trace_to_env():
original_state = {k: os.environ.get(k) for k in TRACE_HEADERS}
try:
inject(os.environ) # traceparent, tracestate를 환경변수에 주입
yield
finally:
for key, original_value in original_state.items():
if original_value is None:
os.environ.pop(key, None)
else:
os.environ[key] = original_value
메인 프로세스에서 워커를 spawn할 때, 현재 트레이스 컨텍스트를 os.environ에 주입한다. 워커는 시작 시 이를 읽어 동일한 트레이스의 자식 스팬을 생성한다. 컨텍스트 매니저로 환경 변수를 원래 상태로 복원한다.
스마트 컨텍스트 감지
def _get_smart_context() -> Context | None:
current_span = trace.get_current_span()
if current_span.get_span_context().is_valid:
return None # 이미 활성 스팬이 있으면 그걸 사용
carrier = {}
if tp := os.environ.get("traceparent", os.environ.get("TRACEPARENT")):
carrier["traceparent"] = tp
if ts := os.environ.get("tracestate", os.environ.get("TRACESTATE")):
carrier["tracestate"] = ts
if not carrier:
carrier = dict(os.environ)
return TraceContextTextMapPropagator().extract(carrier)
활성 스팬이 있으면 그대로 사용하고, 없으면 환경 변수에서 traceparent를 추출한다. 대소문자 모두 시도하는 방어적 코딩이 눈에 띈다.
함수 자동 계측
def instrument_otel(func, span_name, attributes, record_exception):
code_attrs = {
LoadingSpanAttributes.CODE_FUNCTION: func.__qualname__,
LoadingSpanAttributes.CODE_NAMESPACE: func.__module__,
LoadingSpanAttributes.CODE_FILEPATH: func.__code__.co_filename,
LoadingSpanAttributes.CODE_LINENO: str(func.__code__.co_firstlineno),
}
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
tracer = trace.get_tracer(module_name)
ctx = _get_smart_context()
with tracer.start_as_current_span(final_span_name, context=ctx, attributes=code_attrs):
return await func(*args, **kwargs)
sync/async 함수 모두에 대해 자동으로 스팬을 생성하는 데코레이터다. 코드 위치 정보(함수명, 모듈, 파일, 라인 번호)를 정적으로 한 번만 계산하여 스팬 속성에 포함한다.
수동 스팬 생성
def manual_instrument_otel(span_name, start_time, end_time=None, attributes=None, context=None, kind=None):
tracer = trace.get_tracer(__name__)
ctx = context if context is not None else _get_smart_context()
span = tracer.start_span(name=span_name, context=ctx, start_time=start_time)
if attributes:
span.set_attributes(attributes)
span.end(end_time=end_time)
사후 보고용으로, 이미 완료된 연산의 시작/종료 시간을 명시하여 스팬을 생성할 수 있다. CUDA 비동기 연산의 실제 실행 시간을 보고할 때 유용하다.
왜 이 설계인가
-
환경 변수 기반 전파: vLLM은 멀티프로세스 아키텍처(API 서버, 엔진 코어, GPU 워커)를 사용한다. HTTP 헤더가 아닌 환경 변수로 트레이스 컨텍스트를 전파하는 것은, 프로세스 spawn 시점에 자연스럽게 컨텍스트를 상속받기 위한 선택이다.
-
Graceful degradation: OpenTelemetry 패키지가 설치되지 않으면
_IS_OTEL_AVAILABLE = False로 설정하고, 모든 계측 함수가 no-op이 된다. 프로덕션에서 관측 도구 없이도 vLLM이 정상 동작한다. -
BatchSpanProcessor: 스팬을 즉시 전송하지 않고 배치로 모아서 전송한다. LLM 추론은 밀리초 단위의 고빈도 연산이므로, 스팬 전송 오버헤드를 최소화하는 것이 중요하다.
참고 자료
관련 포스트
vLLM 의 다른글
- 이전글 [vLLM] Sleep Mode: GPU 메모리 동적 관리
- 현재글 : [vLLM] Observability: 추적, 프로파일링, 메트릭
- 다음글 [vLLM] Plugin & Hardware: 플러그인 시스템과 하드웨어 플랫폼
댓글