Skip to content

Commit 753d7ba

Browse files
authored
[BugFix] Report prefiller cached tokens in PD proxy (vllm-project#11630)
在这个PR的基础上做了一些优化,vllm-project#10598 优化点: SSE模式下,只有最后一次的chunk的cached token数是正确的,数值=prefill节点的cached token数,而无需每轮chunk都修改。 测试流程: 单机分别部署PD节点: P节点: vllm serve /xxx/Qwen3-8B \ --host 0.0.0.0 \ --port 1000 \ --enable-prefix-caching \ --tensor-parallel-size 1 \ --seed 1024 \ --served-model-name Qwen3-8B \ --max-model-len 40000 \ --max-num-batched-tokens 40000 \ --enable-prompt-tokens-details \ --trust-remote-code \ --gpu-memory-utilization 0.9 \ --kv-transfer-config \ '{"kv_connector": "MooncakeConnectorV1", "kv_role": "kv_producer", "kv_port": "30000", "kv_connector_extra_config": { "prefill": { "dp_size": 1, "tp_size": 1 }, "decode": { "dp_size": 1, "tp_size": 1 } }' D节点: vllm serve /xxx/Qwen3-8B \ --host 0.0.0.0 \ --port 1001 \ --no-enable-prefix-caching \ --tensor-parallel-size 1 \ --seed 1024 \ --served-model-name Qwen3-8B \ --max-model-len 40000 \ --enable-chunked-prefill \ --max-num-batched-tokens 40000 \ --trust-remote-code \ --gpu-memory-utilization 0.9 \ --enable-force-include-usage \ --kv-transfer-config \ '{"kv_connector": "MooncakeConnectorV1", "kv_role": "kv_consumer", "kv_port": "30100", "kv_connector_extra_config": { "prefill": { "dp_size": 1, "tp_size": 1 }, "decode": { "dp_size": 1, "tp_size": 1 } }' 启动: python3 /xxx/vllm-ascend/examples/disaggregated_prefill_v1/load_balance_proxy_server_example.py \ --host 0.0.0.0 \ --port 8898 \ --prefiller-hosts 0.0.0.0 \ --prefiller-ports 1000 \ --decoder-hosts 0.0.0.0 \ --decoder-ports 1001 结果如下: root@test-xxx# curl -X POST http://localhost:8898/v1/chat/completions \ > -H "Content-Type: application/json" \ > -d '{ > "model": "Qwen3-8B", > "messages": [{"role": "user", "content": "The Aegean Sea is an elongated embayment of the Mediterranean Sea, lying between Europes Balkan peninsula and Asia Minor. It is home to thousands of islands, the most famous being Crete, Rhodes, Mykonos, and Santorini. The Aegean is widely regarded as the cradle of ancient Greek civilization. It was here that the Minoans and Mycenaeans built some of Europes earliest advanced societies, and where legends of Troy and Atlantis were born.Today, the sea is known for its crystal-clear waters, white-washed villages perched on cliffs, and a rich maritime heritage that continues to shape the culture of Greece and Turkey."}], > "max_tokens": 10, > "stream": true, > "stream_options": { > "include_usage": true > } > }' data: {"id": "chatcmpl-xxxx", "object": "chat.completion.chunk", "created": 1783499708, "model": "Qwen3-8B", "choi ces": [{"index": 0, "delta": {"role": "assistant", "content": ""}, "logprobs": null, "finish_reason": null}], "usage": {"prompt_tokens": 145, "total_tokens": 145, "completion_tokens": 0, "prompt_tokens_details": {"cached_tokens": 145}}, "prompt_token_ids": null, "prompt_text": null} data: {"id": "chatcmpl-xxxx", "object": "chat.completion.chunk", "created": 1783499708, "model": "Qwen3-8B", "choi ces": [{"index": 0, "delta": {"content": "<think>"}, "logprobs": null, "finish_reason": null, "token_ids": null}], "usage": {"prompt_tokens": 145, "total_tokens": 146, "completion_tokens": 1, "prompt_tokens_details": {"cached_tokens": 145}}} data: {"id": "chatcmpl-xxxx", "object": "chat.completion.chunk", "created": 1783499708, "model": "Qwen3-8B", "choi ces": [{"index": 0, "delta": {"content": "\n"}, "logprobs": null, "finish_reason": null, "token_ids": null}], "usage": {"prompt_tokens": 145, "total_tokens": 147, "completion_tokens": 2, "prompt_tokens_details": {"cached_tokens": 145}}} ...... data: {"id": "chatcmpl-xxxx", "object": "chat.completion.chunk", "created": 1783499708, "model": "Qwen3-8B", "choi ces": [{"index": 0, "delta": {"content": " about"}, "logprobs": null, "finish_reason": "length", "stop_reason": null, "token_ids": null}], "usage": {"prompt_tokens": 145, "total_tokens": 155, "completion_tokens": 10, "prompt_tokens_details": {"cached_tokens": 145}}} data: {"id": "chatcmpl-xxxx", "object": "chat.completion.chunk", "created": 1783499708, "model": "Qwen3-8B", "choi ces": [], "usage": {"prompt_tokens": 145, "total_tokens": 155, "completion_tokens": 10, "prompt_tokens_details": {"cached_tokens": 128}}, "system_fingerprint": "vllm-0.23.0-70fa37d5"} data: [DONE] 仅最后一次总的token统计数{"cached_tokens": 128},(本实验中block size设置的128token)符合预期。 - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@1f486d9 Signed-off-by: LiuY <LiuYuan_jjj@163.com>
1 parent b2dc738 commit 753d7ba

1 file changed

Lines changed: 38 additions & 3 deletions

File tree

examples/disaggregated_prefill_v1/load_balance_proxy_server_example.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,38 @@ class InstanceInfo:
161161
decoder_score: float
162162
decoder_host: str
163163
decoder_port: int
164+
prefiller_cached_tokens: int | None = None
164165

165166

166167
TAINT_PRIORITY = 1e15
167168

169+
170+
def extract_cached_tokens(response_json: dict) -> int | None:
171+
usage = response_json.get("usage") or {}
172+
prompt_tokens_details = usage.get("prompt_tokens_details") or {}
173+
cached_tokens = prompt_tokens_details.get("cached_tokens")
174+
return cached_tokens if isinstance(cached_tokens, int) else None
175+
176+
177+
def update_cached_tokens_in_chunk(chunk_json: dict, cached_tokens: int | None) -> bool:
178+
if cached_tokens is None:
179+
return False
180+
usage = chunk_json.get("usage")
181+
if not isinstance(usage, dict):
182+
return False
183+
prompt_tokens_details = usage.get("prompt_tokens_details")
184+
if not isinstance(prompt_tokens_details, dict):
185+
prompt_tokens_details = {}
186+
usage["prompt_tokens_details"] = prompt_tokens_details
187+
prompt_tokens_details["cached_tokens"] = cached_tokens
188+
return True
189+
190+
191+
def encode_response_chunk(chunk_json: dict, is_sse: bool) -> bytes:
192+
chunk = json.dumps(chunk_json, ensure_ascii=False).encode("utf-8")
193+
return b"data: " + chunk + b"\n\n" if is_sse else chunk
194+
195+
168196
global_args: argparse.Namespace | None = None
169197
shared_scheduler: "SharedProxyScheduler | None" = None
170198
runtime: "WorkerRuntime | None" = None
@@ -922,9 +950,11 @@ async def assign_instances(
922950
await _abort_prefill_selection(runtime, prefiller_key, prefiller_score, is_initial_request=is_initial_request)
923951
raise
924952

925-
kv_transfer_params = response.json().get("kv_transfer_params", {})
953+
response_json = response.json()
954+
kv_transfer_params = response_json.get("kv_transfer_params", {})
926955
if kv_transfer_params:
927956
req_data["kv_transfer_params"] = kv_transfer_params
957+
prefiller_cached_tokens = extract_cached_tokens(response_json)
928958

929959
try:
930960
decoder = await runtime.schedule("pick_decoder", decoder_score)
@@ -943,6 +973,7 @@ async def assign_instances(
943973
decoder_score=decoder_score,
944974
decoder_host=decoder["host"],
945975
decoder_port=decoder["port"],
976+
prefiller_cached_tokens=prefiller_cached_tokens,
946977
)
947978

948979

@@ -987,6 +1018,7 @@ async def generate_stream():
9871018
retry_count = 0
9881019
retry = True
9891020
completion_tokens = 0
1021+
reported_prefiller_cached_tokens = instance_info.prefiller_cached_tokens
9901022

9911023
async def release_prefill_kv_once() -> None:
9921024
nonlocal released_kv
@@ -1018,7 +1050,8 @@ async def release_prefill_kv_once() -> None:
10181050
continue
10191051
if not chunk_str:
10201052
continue
1021-
if chunk_str.startswith("data: "):
1053+
is_sse = chunk_str.startswith("data: ")
1054+
if is_sse:
10221055
chunk_str = chunk_str[len("data: ") :]
10231056
try:
10241057
chunk_json = json.loads(chunk_str)
@@ -1028,6 +1061,8 @@ async def release_prefill_kv_once() -> None:
10281061
continue
10291062
choices = chunk_json.get("choices", [])
10301063
if not choices:
1064+
if update_cached_tokens_in_chunk(chunk_json, reported_prefiller_cached_tokens):
1065+
chunk = encode_response_chunk(chunk_json, is_sse)
10311066
yield chunk
10321067
continue
10331068

@@ -1061,7 +1096,7 @@ async def release_prefill_kv_once() -> None:
10611096
choice["message"]["content"] = generated_token
10621097
else:
10631098
choice["text"] = generated_token
1064-
chunk = json.dumps(chunk_json).encode("utf-8")
1099+
chunk = encode_response_chunk(chunk_json, is_sse)
10651100
yield chunk
10661101
except asyncio.CancelledError:
10671102
logger.warning(

0 commit comments

Comments
 (0)