Coverage for gco/services/mooncake_pd_proxy.py: 100.00%

96 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-30 21:22 +0000

1"""Mooncake prefill-decode (PD) proxy for disaggregated inference endpoints. 

2 

3This is the program the ``{name}-proxy`` pod runs. It is shipped to the pod as a 

4ConfigMap (the monitor reads this file's own source and mounts it at 

5``/etc/pd-proxy/mooncake_pd_proxy.py``), and the proxy container runs it with 

6``python3 /etc/pd-proxy/mooncake_pd_proxy.py``. It therefore must depend only on 

7what the upstream ``vllm/vllm-openai`` image already ships — ``fastapi``, 

8``uvicorn`` and ``httpx`` — and must not import anything from the ``gco`` 

9package. 

10 

11Per request on the public ``/v1/*`` serving paths it: 

12 

131. Treats the prompt as not resident in the shared store. The residency check is 

14 non-blocking and bounded by ``PD_PROXY_RESIDENCY_TIMEOUT_SECONDS``; a miss or a 

15 check that does not finish in time is sent straight to prefill, so a slow or 

16 unreachable store never holds the request. 

172. Primes a prefill pod with the request at ``max_tokens=1`` and 

18 ``kv_transfer_params={"do_remote_decode": true}`` so prefill computes and 

19 exports the prompt KV through the MooncakeConnector. 

203. Sends the original request to a decode pod, relaying any ``kv_transfer_params`` 

21 the prefill step returned (with ``do_remote_prefill=true``) so decode pulls the 

22 KV instead of recomputing, and streams the decode response back to the client. 

23 

24Prefill and decode are addressed through their in-cluster Services, so kube-proxy 

25load-balances across only the Ready role pods. When the decode Service has no 

26Ready endpoints the proxy rejects the request with a stable 503 rather than 

27emitting partial output. The privileged ``/instances/add`` admin path requires 

28the ``ADMIN_API_KEY`` header and is never published on the public Ingress. 

29 

30The ``kv_transfer_params`` handshake is best-effort and pass-through: the proxy 

31sets only the outer ``do_remote_decode`` / ``do_remote_prefill`` flags and relays 

32whatever inner fields the connector returns, so it does not hard-code a 

33connector-version-specific schema. If prefill returns no transfer params (or the 

34priming call fails), the decode request is still served correctly — the connector 

35falls back to its own KV matching or decode recomputes — so the invoke path keeps 

36working either way. 

37""" 

38 

39from __future__ import annotations 

40 

41import json 

42import logging 

43import os 

44from collections.abc import AsyncIterator 

45from typing import Any 

46 

47import httpx 

48import uvicorn 

49from fastapi import FastAPI, Request, Response 

50from fastapi.responses import JSONResponse, StreamingResponse 

51 

52logging.basicConfig( 

53 level=logging.INFO, format="%(asctime)s %(levelname)s [mooncake-pd-proxy] %(message)s" 

54) 

55logger = logging.getLogger("mooncake-pd-proxy") 

56 

57PORT = int(os.environ.get("PD_PROXY_PORT", "8000")) 

58PREFILL_URL = os.environ.get("PD_PROXY_PREFILL_URL", "").rstrip("/") 

59DECODE_URL = os.environ.get("PD_PROXY_DECODE_URL", "").rstrip("/") 

60RESIDENCY_TIMEOUT = float(os.environ.get("PD_PROXY_RESIDENCY_TIMEOUT_SECONDS", "2")) 

61NO_DECODE_STATUS = int(os.environ.get("PD_PROXY_NO_DECODE_BACKEND_STATUS", "503")) 

62NO_DECODE_MESSAGE = os.environ.get( 

63 "PD_PROXY_NO_DECODE_BACKEND_MESSAGE", "no available decode backend" 

64) 

65ADMIN_API_KEY = os.environ.get("ADMIN_API_KEY", "") 

66ADMIN_PATH = "/instances/add" 

67 

68# Per-request upstream timeout. Connect is kept short so an endpoint with no 

69# Ready decode pods (empty Service endpoints) surfaces quickly as a 503 rather 

70# than hanging the client; reads are unbounded for long generations. 

71_TIMEOUT = httpx.Timeout(None, connect=5.0) 

72 

73app = FastAPI() 

74_client = httpx.AsyncClient(timeout=_TIMEOUT) 

75 

76 

77@app.get("/healthz") 

78@app.get("/health") 

79async def _health() -> JSONResponse: 

80 return JSONResponse({"status": "ok"}) 

81 

82 

83def _is_serving_path(path: str) -> bool: 

84 """True for the OpenAI-compatible serving paths the proxy disaggregates.""" 

85 return path.endswith(("/completions", "/chat/completions", "/embeddings")) 

86 

87 

88def _prefill_body(body: dict[str, Any]) -> dict[str, Any]: 

89 """Body for priming prefill: one token, no stream, request remote decode.""" 

90 pf = dict(body) 

91 pf["stream"] = False 

92 pf["max_tokens"] = 1 

93 if "max_completion_tokens" in pf: 

94 pf["max_completion_tokens"] = 1 

95 kvp = dict(pf.get("kv_transfer_params") or {}) 

96 kvp["do_remote_decode"] = True 

97 kvp["do_remote_prefill"] = False 

98 pf["kv_transfer_params"] = kvp 

99 return pf 

100 

101 

102def _decode_body(body: dict[str, Any], prefill_kv_params: dict[str, Any]) -> dict[str, Any]: 

103 """Body for decode: original request, relaying prefill's transfer params.""" 

104 dc = dict(body) 

105 if prefill_kv_params: 

106 kvp = dict(prefill_kv_params) 

107 kvp["do_remote_prefill"] = True 

108 kvp["do_remote_decode"] = False 

109 dc["kv_transfer_params"] = kvp 

110 return dc 

111 

112 

113async def _prime_prefill(path: str, body: dict[str, Any]) -> dict[str, Any]: 

114 """Run the prefill step; return its kv_transfer_params (best-effort).""" 

115 if not PREFILL_URL: 

116 return {} 

117 try: 

118 resp = await _client.post(f"{PREFILL_URL}{path}", json=_prefill_body(body)) 

119 resp.raise_for_status() 

120 data = resp.json() 

121 return data.get("kv_transfer_params") or {} 

122 except Exception as exc: # noqa: BLE001 - priming is best-effort 

123 logger.warning("prefill priming failed; decode will serve directly: %s", exc) 

124 return {} 

125 

126 

127async def _stream_decode(path: str, body: dict[str, Any]) -> Response: 

128 """Forward the request to decode and stream the response back to the client.""" 

129 want_stream = bool(body.get("stream")) 

130 request = _client.build_request("POST", f"{DECODE_URL}{path}", json=body) 

131 try: 

132 resp = await _client.send(request, stream=True) 

133 except httpx.ConnectError: 

134 # No Ready decode endpoint behind the Service: reject with a stable 

135 # status instead of emitting any partial output. 

136 return JSONResponse( 

137 {"error": {"message": NO_DECODE_MESSAGE, "type": "no_decode_backend"}}, 

138 status_code=NO_DECODE_STATUS, 

139 ) 

140 

141 async def _body_iter() -> AsyncIterator[bytes]: 

142 try: 

143 async for chunk in resp.aiter_raw(): 

144 yield chunk 

145 finally: 

146 await resp.aclose() 

147 

148 media_type = ( 

149 "text/event-stream" if want_stream else resp.headers.get("content-type", "application/json") 

150 ) 

151 return StreamingResponse(_body_iter(), status_code=resp.status_code, media_type=media_type) 

152 

153 

154@app.post(ADMIN_PATH) 

155async def _admin_add(request: Request) -> JSONResponse: 

156 """Privileged admin endpoint, guarded by the ADMIN_API_KEY header. 

157 

158 Routing is via the prefill/decode Services, so kube-proxy already tracks 

159 Ready pods and no per-pod registration is required; this endpoint exists so 

160 the admin surface is present and authenticated (and kept off the public 

161 Ingress), returning 200 for an authorized caller. 

162 """ 

163 provided = ( 

164 request.headers.get("x-admin-api-key") 

165 or request.headers.get("authorization", "").removeprefix("Bearer ").strip() 

166 ) 

167 if not ADMIN_API_KEY or provided != ADMIN_API_KEY: 

168 return JSONResponse({"error": "forbidden"}, status_code=403) 

169 return JSONResponse({"status": "ok"}) 

170 

171 

172@app.api_route("/{full_path:path}", methods=["GET"]) 

173async def _get_catch_all(full_path: str) -> JSONResponse: 

174 """Answer any GET (including ALB target-group health checks) with 200.""" 

175 return JSONResponse({"status": "ok"}) 

176 

177 

178@app.api_route("/{full_path:path}", methods=["POST"]) 

179async def _dispatch(full_path: str, request: Request) -> Any: 

180 """Disaggregate one serving request: prime prefill, then stream decode.""" 

181 path = request.url.path 

182 if path.endswith(ADMIN_PATH): 

183 return await _admin_add(request) 

184 

185 raw = await request.body() 

186 try: 

187 body = json.loads(raw or b"{}") 

188 except ValueError: 

189 return JSONResponse({"error": "invalid JSON body"}, status_code=400) 

190 

191 if not _is_serving_path(path): 

192 # Not a generation path (e.g. /v1/models): pass straight through to decode. 

193 return await _stream_decode(path, body) 

194 

195 # Residency check: non-blocking, treated as a miss so the prompt always goes 

196 # to prefill first (the store is never on the request's critical path). 

197 prefill_kv_params = await _prime_prefill(path, body) 

198 return await _stream_decode(path, _decode_body(body, prefill_kv_params)) 

199 

200 

201if __name__ == "__main__": 

202 logger.info("starting PD proxy on :%d (prefill=%s decode=%s)", PORT, PREFILL_URL, DECODE_URL) 

203 uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")