Description
In pkg/kthena-router/connectors/http.go and pkg/kthena-router/connectors/nixl.go (and by extension mooncake.go), the HTTPConnector and NIXLConnector structs maintain mutable request state (prefillRequest, decodeRequest, and decodeRequestBody) as struct fields across calls to Proxy().
When Kthena Router handles disaggregated prefill-decode requests (proxyToPDDisaggregated), connector instances are created by the connector factory (r.connectorFactory.GetConnector(connectorType)) and reused to handle all incoming traffic for that connector type.
When two or more requests are routed through HTTPConnector, NIXLConnector, or MoonCakeConnector concurrently, multiple goroutines concurrently mutate and read h.prefillRequest, h.decodeRequest, n.prefillRequest, and n.decodeRequestBody on the shared connector instance without synchronization. This causes a severe data race and request corruption: one request's decode payload (n.decodeRequestBody or h.decodeRequest) is overwritten by another concurrent request, causing the wrong prompt, messages, and parameters to be sent to the decode engine.
Steps to reproduce the issue
- Deploy Kthena with a
ModelServer configured for PD disaggregation (e.g. using kvConnector: {type: "nixl"} or type: "http" or type: "mooncake").
- Send two concurrent chat completion requests with different prompts/models simultaneously (e.g. Request 1 from User A with prompt
"User A prompt", Request 2 from User B with prompt "User B prompt").
- Request 1 and Request 2 call
connector.Proxy() simultaneously on separate goroutines.
- Request 1 sets
n.decodeRequestBody to Request 1's payload.
- Request 2 sets
n.decodeRequestBody to Request 2's payload before Request 1 begins its decode phase.
- Request 1 proceeds to the decode phase (
n.buildDecodeRequest(c, n.decodeRequestBody, kvTransferParams)) and reads n.decodeRequestBody, which now contains Request 2's payload.
Evidence and production path
Production execution path:
-
Router invokes proxyToPDDisaggregated:
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/router/router.go#L1363-L1420
-
In HTTPConnector.Proxy:
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/http.go#L30-L33
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/http.go#L74-L88
h.decodeRequest and h.prefillRequest are stored directly on the HTTPConnector struct instance.
-
In NIXLConnector.Proxy (and MoonCakeConnector which aliases NIXLConnector):
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/nixl.go#L35-L39
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/nixl.go#L65-L67
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/nixl.go#L79
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/nixl.go#L107-L108
n.prefillRequest and n.decodeRequestBody are stored on the NIXLConnector struct instance and later read in n.prefill() and n.buildDecodeRequest().
Under concurrent traffic:
- Goroutine 1 writes
n.decodeRequestBody = body1
- Goroutine 2 writes
n.decodeRequestBody = body2
- Goroutine 1 reads
n.decodeRequestBody in n.buildDecodeRequest and sends body2 to the decode pod.
Describe the results you received and expected
Expected Behavior:
KVConnector implementations should be stateless and thread-safe. Prefill and decode requests/bodies should be kept as local variables within Proxy() and passed as arguments, ensuring zero shared state across concurrent requests.
Actual Behavior:
Shared struct fields cause data races and cross-request contamination where one request's decode phase uses another request's body payload.
What version of Kthena are you using?
v0.2.0 (main branch commit 713a4a5814526d19b45781a742ea3521d9657065)
Any other relevant information
Affects HTTPConnector, NIXLConnector, and MoonCakeConnector. By contrast, SGLangConnector (pkg/kthena-router/connectors/sglang.go) correctly uses local variables and avoids shared struct fields.
Description
In
pkg/kthena-router/connectors/http.goandpkg/kthena-router/connectors/nixl.go(and by extensionmooncake.go), theHTTPConnectorandNIXLConnectorstructs maintain mutable request state (prefillRequest,decodeRequest, anddecodeRequestBody) as struct fields across calls toProxy().When Kthena Router handles disaggregated prefill-decode requests (
proxyToPDDisaggregated), connector instances are created by the connector factory (r.connectorFactory.GetConnector(connectorType)) and reused to handle all incoming traffic for that connector type.When two or more requests are routed through
HTTPConnector,NIXLConnector, orMoonCakeConnectorconcurrently, multiple goroutines concurrently mutate and readh.prefillRequest,h.decodeRequest,n.prefillRequest, andn.decodeRequestBodyon the shared connector instance without synchronization. This causes a severe data race and request corruption: one request's decode payload (n.decodeRequestBodyorh.decodeRequest) is overwritten by another concurrent request, causing the wrong prompt, messages, and parameters to be sent to the decode engine.Steps to reproduce the issue
ModelServerconfigured for PD disaggregation (e.g. usingkvConnector: {type: "nixl"}ortype: "http"ortype: "mooncake")."User A prompt", Request 2 from User B with prompt"User B prompt").connector.Proxy()simultaneously on separate goroutines.n.decodeRequestBodyto Request 1's payload.n.decodeRequestBodyto Request 2's payload before Request 1 begins its decode phase.n.buildDecodeRequest(c, n.decodeRequestBody, kvTransferParams)) and readsn.decodeRequestBody, which now contains Request 2's payload.Evidence and production path
Production execution path:
Router invokes
proxyToPDDisaggregated:https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/router/router.go#L1363-L1420
In
HTTPConnector.Proxy:https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/http.go#L30-L33
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/http.go#L74-L88
h.decodeRequestandh.prefillRequestare stored directly on theHTTPConnectorstruct instance.In
NIXLConnector.Proxy(andMoonCakeConnectorwhich aliasesNIXLConnector):https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/nixl.go#L35-L39
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/nixl.go#L65-L67
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/nixl.go#L79
https://github.com/volcano-sh/kthena/blob/713a4a5814526d19b45781a742ea3521d9657065/pkg/kthena-router/connectors/nixl.go#L107-L108
n.prefillRequestandn.decodeRequestBodyare stored on theNIXLConnectorstruct instance and later read inn.prefill()andn.buildDecodeRequest().Under concurrent traffic:
n.decodeRequestBody = body1n.decodeRequestBody = body2n.decodeRequestBodyinn.buildDecodeRequestand sendsbody2to the decode pod.Describe the results you received and expected
Expected Behavior:
KVConnectorimplementations should be stateless and thread-safe. Prefill and decode requests/bodies should be kept as local variables withinProxy()and passed as arguments, ensuring zero shared state across concurrent requests.Actual Behavior:
Shared struct fields cause data races and cross-request contamination where one request's decode phase uses another request's body payload.
What version of Kthena are you using?
v0.2.0 (
mainbranch commit713a4a5814526d19b45781a742ea3521d9657065)Any other relevant information
Affects
HTTPConnector,NIXLConnector, andMoonCakeConnector. By contrast,SGLangConnector(pkg/kthena-router/connectors/sglang.go) correctly uses local variables and avoids shared struct fields.