본문으로 건너뛰기

[SGLang] gRPC 서버: 분산 추론을 위한 고성능 통신 계층

들어가며

SGLang은 기본적으로 HTTP/REST 기반의 OpenAI 호환 API를 제공한다. 하지만 대규모 분산 추론 환경에서는 HTTP의 오버헤드가 병목이 될 수 있다. SGLang은 이를 해결하기 위해 --grpc-mode 플래그 하나로 gRPC 서버를 활성화할 수 있는 구조를 제공한다.

gRPC 서버는 두 가지 주요 역할을 담당한다. 첫째, SGLang Model Gateway(Rust 기반 라우터)와 Python Scheduler 사이의 내부 통신 프로토콜로 동작한다. 둘째, 멀티모달 입력을 처리하는 Encoder 서버의 분산 통신 계층으로 활용된다. 이 글에서는 python/sglang/srt/entrypoints/grpc_server.py와 관련 코드를 분석하며 SGLang이 gRPC를 어떻게 활용하는지 살펴본다.

gRPC vs HTTP 비교

LLM 추론 서버에서 gRPC와 HTTP/REST의 차이를 이해하는 것이 중요하다.

HTTP/REST 요청 흐름:
┌────────┐   JSON(text)   ┌────────────┐   JSON parse   ┌───────────┐
│ Client ├───────────────►│ HTTP Server├──────────────►│ Scheduler │
│        │◄───────────────┤  (uvicorn) │◄──────────────┤           │
└────────┘   JSON(text)   └────────────┘  JSON serialize └───────────┘
             + SSE for streaming

gRPC 요청 흐름:
┌────────┐  Protobuf(bin) ┌────────────┐   zero-copy    ┌───────────┐
│ Client ├───────────────►│ gRPC Server├──────────────►│ Scheduler │
│        │◄═══════════════┤  (HTTP/2)  │◄══════════════┤           │
└────────┘  Stream(bin)   └────────────┘   structured   └───────────┘
             bidirectional streaming
항목 HTTP/REST gRPC
프로토콜 HTTP/1.1 HTTP/2
직렬화 JSON (텍스트) Protobuf (바이너리)
스키마 비공식 (OpenAPI) 엄격한 .proto 정의
Streaming SSE (서버→클라이언트 단방향) 양방향 Streaming
연결 관리 요청당 또는 Keep-Alive Multiplexed 연결
타입 안전성 런타임 검증 컴파일 타임 검증
메시지 크기 상대적으로 큰 JSON 3-10배 작은 바이너리
Latency 직렬화/역직렬화 오버헤드 최소 오버헤드

LLM 추론에서 특히 중요한 차이는 Streaming이다. HTTP/REST에서는 SSE(Server-Sent Events)로 토큰을 하나씩 전달하지만, 텍스트 기반이라 파싱 오버헤드가 있다. gRPC의 Server Streaming RPC는 바이너리 Protobuf 메시지를 HTTP/2 프레임으로 직접 전달하므로, 토큰 단위 전달에서 더 효율적이다.

핵심 코드 분석

gRPC 서버 진입점

python/sglang/srt/entrypoints/grpc_server.py는 gRPC 서버의 진입점이다. 핵심 구현은 smg-grpc-servicer 패키지에 위임하고, 이 파일은 Metrics 서버 관리와 서버 생명주기를 담당한다.

async def serve_grpc(server_args, model_info=None):
    """Start the standalone gRPC server with integrated scheduler."""
    try:
        from smg_grpc_servicer.sglang.server import serve_grpc as _serve_grpc
    except ImportError as e:
        raise ImportError(
            "gRPC mode requires the smg-grpc-servicer package. "
            "If not installed, run: pip install smg-grpc-servicer[sglang]. "
            "If already installed, there may be a broken import due to a "
            "version mismatch — see the chained exception above for details."
        ) from e

이 구조가 흥미로운 점은, gRPC 구현체를 별도 패키지(smg-grpc-servicer)로 분리했다는 것이다. SGLang 코어는 HTTP 서버만 포함하고, gRPC는 선택적 의존성으로 설치한다. 이를 통해 gRPC가 필요 없는 단독 배포 환경에서 불필요한 의존성을 피할 수 있다.

Prometheus Metrics 통합

gRPC 모드에서는 HTTP 서버가 없으므로, Prometheus metrics를 별도의 경량 HTTP 서버로 노출한다.

async def _start_metrics_server(host: str, port: int):
    from aiohttp import web
    from prometheus_client import CollectorRegistry, multiprocess
    from prometheus_client.openmetrics.exposition import (
        CONTENT_TYPE_LATEST, generate_latest,
    )

    async def metrics_handler(request):
        registry = CollectorRegistry()
        multiprocess.MultiProcessCollector(registry)
        data = generate_latest(registry)
        return web.Response(
            body=data,
            headers={"Content-Type": CONTENT_TYPE_LATEST},
        )

    app = web.Application()
    app.router.add_get("/metrics", metrics_handler)

매 요청마다 CollectorRegistry를 새로 생성하고 MultiProcessCollector를 붙이는 패턴이 사용된다. 이는 prometheus_client 공식 문서가 권장하는 multiprocess 환경 패턴으로, PROMETHEUS_MULTIPROC_DIR에서 최신 데이터를 읽어 올 수 있다. Metrics 서버는 기본적으로 gRPC 포트 + 1에서 동작한다.

metrics_port = (
    server_args.metrics_http_port
    if server_args.metrics_http_port is not None
    else server_args.port + 1
)
metrics_runner = await _start_metrics_server(server_args.host, metrics_port)

gRPC 서비스 정의: Scheduler Protocol

SGLang의 gRPC 서비스는 sglang_scheduler.proto에 정의되어 있다. 생성된 Go 코드(sglang_scheduler_grpc.pb.go)를 통해 서비스 인터페이스를 확인할 수 있다.

// Service definition for SGLang scheduler communication
// This protocol bridges the Rust router and Python scheduler
type SglangSchedulerClient interface {
    // Submit a generation request (supports streaming)
    Generate(ctx context.Context, in *GenerateRequest, opts ...grpc.CallOption) (
        grpc.ServerStreamingClient[GenerateResponse], error)
    // Submit an embedding request
    Embed(ctx context.Context, in *EmbedRequest, opts ...grpc.CallOption) (
        *EmbedResponse, error)
    // Health check and metrics
    HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (
        *HealthCheckResponse, error)
    // Abort a running request
    Abort(ctx context.Context, in *AbortRequest, opts ...grpc.CallOption) (
        *AbortResponse, error)
    // Get model information
    GetModelInfo(ctx context.Context, in *GetModelInfoRequest, opts ...grpc.CallOption) (
        *GetModelInfoResponse, error)
    // Get server information
    GetServerInfo(ctx context.Context, in *GetServerInfoRequest, opts ...grpc.CallOption) (
        *GetServerInfoResponse, error)
}

6개의 RPC 메서드 중 Generate만 Server Streaming RPC이고, 나머지는 Unary RPC다. 주석이 명시하듯 이 프로토콜은 Rust 라우터와 Python Scheduler를 연결하는 브릿지 역할을 한다.

Streaming RPC: 실시간 토큰 전달

Generate RPC의 응답 메시지는 oneof 패턴으로 세 가지 타입을 구분한다.

type GenerateResponse struct {
    RequestId string
    // oneof response
    Response isGenerateResponse_Response  // Chunk | Complete | Error
}

type GenerateStreamChunk struct {
    TokenIds         []uint32        // 생성된 토큰 ID (증분)
    PromptTokens     int32           // 누적 프롬프트 토큰 수
    CompletionTokens int32           // 누적 완성 토큰 수
    CachedTokens     int32           // 캐시 히트 토큰 수
    OutputLogprobs   *OutputLogProbs // 출력 로그 확률 (요청 시)
    HiddenStates     []float32       // 히든 스테이트 (요청 시)
    InputLogprobs    *InputLogProbs  // 입력 로그 확률 (첫 청크만)
    Index            uint32          // n>1일 때 순서 식별
}

JSON 기반 SSE에서는 토큰을 텍스트로 직렬화해야 하지만, Protobuf에서는 TokenIdsuint32 배열로 직접 전달한다. 이 차이가 고처리량 환경에서 유의미한 성능 차이를 만든다. CachedTokens 필드는 SGLang의 RadixAttention KV 캐시 히트 정보를 실시간으로 전달하는 데 활용된다.

분산 환경 활용: Encoder gRPC Server

python/sglang/srt/disaggregation/encode_grpc_server.py는 EPD(Encode-Prefill-Decode) 분리 모드에서 멀티모달 인코딩을 담당하는 gRPC 서버다.

class SGLangEncoderServer(SGLangEncoderServicer):
    def __init__(self, encoder: MMEncoder, send_sockets: List[zmq.Socket],
                 server_args: ServerArgs):
        self.encoder = encoder
        self.send_sockets = send_sockets
        self.server_args = server_args

    async def Encode(self, request, context):
        for socket in self.send_sockets:
            await socket.send_pyobj(request_dict)

        (nbytes, embedding_len, embedding_dim,
         error_msg, error_code) = await self.encoder.encode(
            mm_items=list(request.mm_items),
            modality=Modality.IMAGE,
            req_id=request.req_id,
            num_parts=request.num_parts,
            part_idx=request.part_idx,
        )

이 구현에서 주목할 점은 gRPC와 ZeroMQ를 함께 사용하는 하이브리드 아키텍처다. gRPC는 외부 인터페이스(인코딩 요청 수신)를, ZeroMQ는 내부 프로세스 간 통신(TP 워커 간 조율)을 담당한다.

서버 초기화 코드를 보면 이 구조가 명확해진다.

async def serve_grpc_encoder(server_args: ServerArgs):
    server = grpc.aio.server(
        futures.ThreadPoolExecutor(max_workers=10),
        options=[
            ("grpc.max_send_message_length", 1024 * 1024 * 256),  # 256MB
            ("grpc.max_receive_message_length", 1024 * 1024 * 256),
        ],
    )
    # Health check + Reflection 등록
    health_servicer = EncoderHealthServicer()
    health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
    reflection.enable_server_reflection(SERVICE_NAMES, server)

메시지 크기 제한을 256MB로 설정한 것은, 멀티모달 임베딩 데이터가 대용량일 수 있기 때문이다. 또한 grpc.health.v1.Health 서비스와 Server Reflection을 함께 등록하여 Kubernetes 헬스 프로브와 동적 서비스 탐색을 지원한다.

SGLang Model Gateway: Rust gRPC 라우터

SGLang Model Gateway는 Rust로 구현된 고성능 라우터로, gRPC를 통해 Python Scheduler와 통신한다. sgl-model-gateway/src/routers/grpc/ 아래의 코드 구조를 보면 아키텍처가 드러난다.

// client.rs — Polymorphic gRPC client
pub enum GrpcClient {
    Sglang(SglangSchedulerClient),
    Vllm(VllmEngineClient),
}

SGLang뿐 아니라 vLLM 백엔드도 같은 gRPC 인터페이스로 추상화한다. 라우터는 RequestExecutionStage에서 Single(단일 워커) 또는 DualDispatch(Prefill + Decode 분리) 모드를 지원하며, gRPC Streaming을 통해 응답을 실시간으로 클라이언트에게 전달한다.

pub(crate) enum ExecutionMode {
    Single,        // 단일 워커 실행
    DualDispatch,  // PD 분리: prefill + decode 워커
}

왜 gRPC인가 -- 성능 비교

SGLang이 gRPC를 도입한 핵심 이유는 세 가지다.

1. 직렬화 효율성: GenerateRequest 메시지에는 SamplingParams(temperature, top_p, top_k 등 20개 이상 필드), TokenizedInput, MultimodalInputs 등이 포함된다. JSON으로 직렬화하면 필드명이 반복되지만, Protobuf는 필드 번호 기반 바이너리 인코딩을 사용해 메시지 크기가 3-10배 작다.

2. Streaming 성능: LLM의 토큰 생성 속도가 초당 수십-수백 토큰에 달하는 환경에서, SSE의 텍스트 파싱 오버헤드는 무시할 수 없다. gRPC Server Streaming은 HTTP/2 프레임 위에서 바이너리 메시지를 직접 전달하므로 토큰당 오버헤드가 최소화된다.

3. 다중 언어 연동: SGLang의 아키텍처는 Python(Scheduler), Rust(Router/Gateway), Go(Bindings)로 구성된다. Protobuf의 코드 생성으로 모든 언어에서 동일한 타입 정의를 공유하며, 인터페이스 불일치로 인한 버그를 컴파일 타임에 차단한다.

관련 포스트

참고

댓글

관련 포스트

SGLang 의 다른글