Files
enginex-ascend-910-vllm/vllm_ascend/patch/platform/patch_minimax_usage_accounting.py
Sun Ruoxi 7f8a1b1f7a init v0.23.0
Signed-off-by: Sun Ruoxi <sunruoxi@4paradigm.com>
2026-08-27 15:11:51 +08:00

463 lines
15 KiB
Python

#
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
# This file is a part of the vllm-ascend project.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# MiniMax-M2 usage accounting: backport reasoning-token usage details.
#
from __future__ import annotations
import json
from collections.abc import AsyncIterator, Sequence
from dataclasses import dataclass
from types import MethodType
from typing import Any
from vllm.entrypoints.openai.chat_completion import protocol as chat_protocol
from vllm.entrypoints.openai.chat_completion import serving as chat_serving
from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat
from vllm.entrypoints.openai.engine import protocol as engine_protocol
from vllm.reasoning import minimax_m2_reasoning_parser as minimax_parser
_MINIMAX_REASONING_PARSER_TYPES = (
minimax_parser.MiniMaxM2ReasoningParser,
minimax_parser.MiniMaxM2AppendThinkReasoningParser,
)
class CompletionTokenUsageInfo(engine_protocol.OpenAIBaseModel):
reasoning_tokens: int | None = None
audio_tokens: int | None = None
accepted_prediction_tokens: int | None = None
rejected_prediction_tokens: int | None = None
class UsageInfo(engine_protocol.UsageInfo):
completion_tokens_details: CompletionTokenUsageInfo | None = None
CompletionTokenUsageInfo.__module__ = engine_protocol.__name__
UsageInfo.__module__ = engine_protocol.__name__
# The OpenAI usage schema is process-wide. Keep only this schema backfill
# global; the expensive token tracking below is bound to MiniMax instances.
engine_protocol.CompletionTokenUsageInfo = CompletionTokenUsageInfo
engine_protocol.UsageInfo = UsageInfo
chat_protocol.UsageInfo = UsageInfo
chat_serving.CompletionTokenUsageInfo = CompletionTokenUsageInfo
chat_serving.UsageInfo = UsageInfo
def _rebuild_model_field(model_cls, field_name: str, annotation) -> None:
model_cls.__annotations__[field_name] = annotation
model_cls.model_fields[field_name].annotation = annotation
model_cls.model_rebuild(force=True)
_rebuild_model_field(chat_protocol.ChatCompletionResponse, "usage", UsageInfo)
_rebuild_model_field(chat_protocol.ChatCompletionStreamResponse, "usage", UsageInfo | None)
_rebuild_model_field(engine_protocol.RequestResponseMetadata, "final_usage_info", UsageInfo | None)
def _count_minimax_reasoning_tokens(
token_ids: Sequence[int],
end_token_id: int | None,
) -> int:
if end_token_id is None:
return 0
for idx, token_id in enumerate(token_ids):
if token_id == end_token_id:
return idx
return len(token_ids)
def _patched_count_reasoning_tokens(self, token_ids: Sequence[int]) -> int:
return _count_minimax_reasoning_tokens(token_ids, self.end_token_id)
minimax_parser.MiniMaxM2ReasoningParser.count_reasoning_tokens = _patched_count_reasoning_tokens
minimax_parser.MiniMaxM2AppendThinkReasoningParser.count_reasoning_tokens = _patched_count_reasoning_tokens
def _count_minimax_reasoning_tokens_for_usage(
token_ids: Sequence[int],
reasoning_parser,
) -> int | None:
reasoning_parser = _resolve_reasoning_parser(reasoning_parser)
if reasoning_parser is None or not _is_minimax_reasoning_parser(reasoning_parser):
return None
count_reasoning_tokens = getattr(reasoning_parser, "count_reasoning_tokens", None)
if count_reasoning_tokens is None:
return None
return count_reasoning_tokens(token_ids)
def _resolve_reasoning_parser(reasoning_parser):
if reasoning_parser is None:
return None
return getattr(reasoning_parser, "reasoning_parser", reasoning_parser)
def _is_minimax_reasoning_parser(reasoning_parser) -> bool:
return isinstance(
_resolve_reasoning_parser(reasoning_parser),
_MINIMAX_REASONING_PARSER_TYPES,
)
def _clamp_reasoning_tokens(
reasoning_tokens: int | None,
completion_tokens: int,
) -> int | None:
if reasoning_tokens is None:
return None
return max(0, min(reasoning_tokens, completion_tokens))
def _make_usage_info(
self,
*,
prompt_tokens: int,
completion_tokens: int,
num_cached_tokens: int | None = None,
reasoning_tokens: int | None = None,
) -> UsageInfo:
usage = UsageInfo(
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=prompt_tokens + completion_tokens,
)
reasoning_tokens = _clamp_reasoning_tokens(reasoning_tokens, completion_tokens)
if reasoning_tokens is not None:
usage.completion_tokens_details = CompletionTokenUsageInfo(reasoning_tokens=reasoning_tokens)
if self.enable_prompt_tokens_details and num_cached_tokens is not None:
usage.prompt_tokens_details = chat_serving.PromptTokenUsageInfo(cached_tokens=num_cached_tokens)
return usage
def _is_minimax_reasoning_parser_cls(reasoning_parser_cls) -> bool:
return isinstance(reasoning_parser_cls, type) and issubclass(
reasoning_parser_cls,
_MINIMAX_REASONING_PARSER_TYPES,
)
@dataclass
class _UsageTrackingState:
completion_tokens: list[int]
raw_output_token_ids: list[list[int]]
reasoning_parser: Any
enable_prompt_tokens_details: bool = False
num_prompt_tokens: int = 0
num_cached_tokens: int | None = None
final_res: Any = None
def _create_usage_tracking_state(
num_choices: int,
reasoning_parser,
enable_prompt_tokens_details: bool = False,
) -> _UsageTrackingState:
return _UsageTrackingState(
completion_tokens=[0] * num_choices,
raw_output_token_ids=[[] for _ in range(num_choices)],
reasoning_parser=reasoning_parser,
enable_prompt_tokens_details=enable_prompt_tokens_details,
)
def _update_usage_tracking_state(
state: _UsageTrackingState,
res,
) -> None:
if res.prompt_token_ids is not None:
num_prompt_tokens = len(res.prompt_token_ids)
if res.encoder_prompt_token_ids is not None:
num_prompt_tokens += len(res.encoder_prompt_token_ids)
state.num_prompt_tokens = num_prompt_tokens
if state.num_cached_tokens is None:
state.num_cached_tokens = res.num_cached_tokens
state.final_res = res
for output in res.outputs:
if 0 <= output.index < len(state.completion_tokens):
token_ids = chat_serving.as_list(output.token_ids)
state.completion_tokens[output.index] += len(token_ids)
state.raw_output_token_ids[output.index].extend(token_ids)
async def _tracked_result_generator(
result_generator: AsyncIterator,
state: _UsageTrackingState,
):
async for res in result_generator:
_update_usage_tracking_state(state, res)
yield res
def _sum_reasoning_tokens_for_usage(
raw_output_token_ids: list[list[int]],
reasoning_parser,
) -> int | None:
if reasoning_parser is None:
return None
reasoning_token_counts = [
_count_minimax_reasoning_tokens_for_usage(token_ids, reasoning_parser) for token_ids in raw_output_token_ids
]
if all(reasoning_tokens is None for reasoning_tokens in reasoning_token_counts):
return None
return sum(reasoning_tokens or 0 for reasoning_tokens in reasoning_token_counts)
def _reasoning_tokens_for_choice(
state: _UsageTrackingState,
choice_index: int,
) -> int | None:
if state.reasoning_parser is None:
return None
if not 0 <= choice_index < len(state.raw_output_token_ids):
return None
return _count_minimax_reasoning_tokens_for_usage(
state.raw_output_token_ids[choice_index],
state.reasoning_parser,
)
def _make_full_response_usage(
self,
state: _UsageTrackingState,
) -> UsageInfo | None:
if state.final_res is None:
return None
return self._make_usage_info(
prompt_tokens=state.num_prompt_tokens,
completion_tokens=sum(state.completion_tokens),
num_cached_tokens=state.num_cached_tokens,
reasoning_tokens=_sum_reasoning_tokens_for_usage(
state.raw_output_token_ids,
state.reasoning_parser,
),
)
def _usage_reasoning_tokens_for_stream_chunk(
state: _UsageTrackingState,
chunk: dict[str, Any],
completion_tokens: int,
) -> int | None:
if state.reasoning_parser is None:
return None
choices = chunk.get("choices") or []
if choices:
choice_index = choices[0].get("index", 0)
reasoning_tokens = _reasoning_tokens_for_choice(state, choice_index)
else:
reasoning_tokens = _sum_reasoning_tokens_for_usage(
state.raw_output_token_ids,
state.reasoning_parser,
)
return _clamp_reasoning_tokens(reasoning_tokens, completion_tokens)
def _inject_stream_usage_details(
data: str,
state: _UsageTrackingState,
) -> str:
prefix = "data: "
suffix = "\n\n"
if not data.startswith(prefix):
return data
payload = data[len(prefix) :]
if payload.endswith(suffix):
payload = payload[: -len(suffix)]
if payload == "[DONE]":
return data
try:
chunk = json.loads(payload)
except json.JSONDecodeError:
return data
usage = chunk.get("usage")
if not isinstance(usage, dict):
return data
updated_usage = False
if state.enable_prompt_tokens_details and state.num_cached_tokens is not None:
usage["prompt_tokens_details"] = {
"cached_tokens": state.num_cached_tokens,
}
updated_usage = True
completion_tokens = usage.get("completion_tokens") or 0
reasoning_tokens = _usage_reasoning_tokens_for_stream_chunk(
state,
chunk,
completion_tokens,
)
if reasoning_tokens is not None:
usage["completion_tokens_details"] = {
"reasoning_tokens": reasoning_tokens,
}
updated_usage = True
if not updated_usage:
return data
return f"{prefix}{json.dumps(chunk, ensure_ascii=False)}{suffix}"
async def _wrapped_chat_completion_stream_generator(
self,
request: chat_protocol.ChatCompletionRequest,
result_generator: AsyncIterator,
request_id: str,
model_name: str,
conversation,
tokenizer,
request_metadata: engine_protocol.RequestResponseMetadata,
reasoning_parser=None,
**extra_kwargs: Any,
):
original_stream_generator = self._ascend_original_chat_completion_stream_generator
num_choices = 1 if request.n is None else request.n
state = _create_usage_tracking_state(
num_choices,
reasoning_parser,
enable_prompt_tokens_details=self.enable_prompt_tokens_details,
)
async for data in original_stream_generator(
request,
_tracked_result_generator(result_generator, state),
request_id,
model_name,
conversation,
tokenizer,
request_metadata,
reasoning_parser,
**extra_kwargs,
):
yield _inject_stream_usage_details(data, state)
usage = _make_full_response_usage(self, state)
if usage is not None:
request_metadata.final_usage_info = usage
async def _wrapped_chat_completion_full_generator(
self,
request: chat_protocol.ChatCompletionRequest,
result_generator: AsyncIterator,
request_id: str,
model_name: str,
conversation,
tokenizer,
request_metadata: engine_protocol.RequestResponseMetadata,
reasoning_parser=None,
):
original_full_generator = self._ascend_original_chat_completion_full_generator
num_choices = 1 if request.n is None else request.n
state = _create_usage_tracking_state(
num_choices,
reasoning_parser,
enable_prompt_tokens_details=self.enable_prompt_tokens_details,
)
response = await original_full_generator(
request,
_tracked_result_generator(result_generator, state),
request_id,
model_name,
conversation,
tokenizer,
request_metadata,
reasoning_parser,
)
if not isinstance(response, chat_protocol.ChatCompletionResponse):
return response
usage = _make_full_response_usage(self, state)
if usage is None:
return response
response.usage = usage
request_metadata.final_usage_info = usage
return response
_wrapped_chat_completion_stream_generator.__module__ = OpenAIServingChat.__module__
_wrapped_chat_completion_stream_generator.__qualname__ = (
f"{OpenAIServingChat.__qualname__}.chat_completion_stream_generator"
)
_wrapped_chat_completion_full_generator.__module__ = OpenAIServingChat.__module__
_wrapped_chat_completion_full_generator.__qualname__ = (
f"{OpenAIServingChat.__qualname__}.chat_completion_full_generator"
)
def _should_patch_chat_usage_instance(self) -> bool:
return _is_minimax_reasoning_parser_cls(self.reasoning_parser_cls)
def _patch_chat_usage_instance(self) -> None:
if getattr(self, "_ascend_minimax_usage_patched", False):
return
self._make_usage_info = MethodType(_make_usage_info, self)
self._ascend_original_chat_completion_stream_generator = MethodType(
OpenAIServingChat.chat_completion_stream_generator,
self,
)
self._ascend_original_chat_completion_full_generator = MethodType(
OpenAIServingChat.chat_completion_full_generator,
self,
)
self.chat_completion_stream_generator = MethodType(
_wrapped_chat_completion_stream_generator,
self,
)
self.chat_completion_full_generator = MethodType(
_wrapped_chat_completion_full_generator,
self,
)
self._ascend_minimax_usage_patched = True
class _ReasoningParserClsDescriptor:
def __init__(self, default_value=None):
self.default_value = default_value
def __get__(self, instance, owner=None):
if instance is None:
return self.default_value
return instance.__dict__.get("_ascend_reasoning_parser_cls", self.default_value)
def __set__(self, instance, value) -> None:
instance.__dict__["_ascend_reasoning_parser_cls"] = value
if _is_minimax_reasoning_parser_cls(value):
_patch_chat_usage_instance(instance)
_current_reasoning_parser_cls = OpenAIServingChat.__dict__.get("reasoning_parser_cls")
if not isinstance(_current_reasoning_parser_cls, _ReasoningParserClsDescriptor):
OpenAIServingChat.reasoning_parser_cls = _ReasoningParserClsDescriptor(_current_reasoning_parser_cls)