Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions ais_bench/benchmark/calculators/base_perf_metric_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ def _add_units_to_metrics(self, metrics: dict) -> dict:
"ITL": ms,
"InputTokens": None,
"OutputTokens": None,
"PrefillTokenThroughput": unit_token,
"OutputTokenThroughput": unit_token,
"PrefillTokenThroughput": unit_token,
}

for metric, values in metrics.items():
Expand Down Expand Up @@ -249,7 +249,6 @@ def _add_units_to_common_metrics(self, common_metrics: dict) -> dict:
"Max Concurrency": None,
"Request Throughput": " req/s",
"Total Input Tokens": None,
"Prefill Token Throughput": unit_token,
"Input Token Throughput": unit_token,
"Total Output Tokens": None,
"Output Token Throughput": unit_token,
Expand Down Expand Up @@ -362,13 +361,29 @@ def convert_result(self, result: dict) -> dict:
else:
ans[mapping_value].append(value)

# Calculate per-request Prefill Token Throughput from input_tokens and ttft
raw_input_tokens = result.get("input_tokens", [])
raw_ttft = result.get("ttft", [])
if raw_input_tokens and raw_ttft:
prefill_throughputs = []
for inp, ttft in zip(raw_input_tokens, raw_ttft):
if isinstance(inp, list):
inp = sum(inp)
if ttft and ttft > 0:
prefill_throughputs.append(inp / ttft)
else:
prefill_throughputs.append(0.0)
ans["PrefillTokenThroughput"] = prefill_throughputs

for key in ["ITL"]:
if not ans[key] or (isinstance(ans[key][0], np.ndarray) and not ans[key][0].any()):
ans.pop(key)

for key in ["TTFT", "TPOT"]:
if math.isclose(sum(ans[key]), 0):
ans.pop(key)
if key == "TTFT":
ans.pop("PrefillTokenThroughput", None)

return ans

Expand Down Expand Up @@ -428,7 +443,6 @@ def _calc_common_metrics(self):
"Max Concurrency",
"Request Throughput",
"Total Input Tokens",
"Prefill Token Throughput",
"Total Generated Tokens",
"Input Token Throughput",
"Output Token Throughput",
Expand Down Expand Up @@ -478,20 +492,6 @@ def _calc_common_metrics(self):
)
self.logger.debug(f"Stage {stage_name} - Total Input Tokens: {self.common_metrics['Total Input Tokens'][stage_name]}")

if (
self.common_metrics["Total Input Tokens"][stage_name] != 0
and self.result[stage_name].get("TTFT") is not None
):
self.common_metrics["Prefill Token Throughput"][stage_name] = round(
self.common_metrics["Total Input Tokens"][stage_name]
/ sum(self.result[stage_name]["TTFT"]),
4,
)
self.logger.debug(f"Stage {stage_name} - Prefill Token Throughput: {self.common_metrics['Prefill Token Throughput'][stage_name]} token/s")
else:
self.common_metrics.pop("Prefill Token Throughput", None)
self.logger.debug(f"Stage {stage_name} - Prefill Token Throughput: Not calculated (insufficient data)")

self.common_metrics["Total Generated Tokens"][stage_name] = sum(
self.result[stage_name]["OutputTokens"]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Explanations of key statistical indicators are as follows:
- **InputTokens**: The number of input tokens in the request.
- **OutputTokens**: The number of output tokens generated by the request.
- **OutputTokenThroughput**: The throughput of output tokens (in tokens per second, Token/s).
- **PrefillTokenThroughput**: The token throughput during the Prefill stage (in tokens per second, Token/s), calculated per request.
- **Tokenizer**: The time consumed for Tokenizer encoding.
- **Detokenizer**: The time consumed for Detokenizer decoding.

Expand All @@ -24,6 +25,7 @@ Explanations of key statistical indicators are as follows:
| InputTokens | Stage for this parameter | Average length of input tokens | Maximum length of input tokens | Minimum length of input tokens | Median length of input tokens | 75th-percentile length of input tokens | 90th-percentile length of input tokens | 99th-percentile length of input tokens | Test data volume (from input parameters) |
| OutputTokens | Stage for this parameter | Average length of output tokens | Maximum length of output tokens | Minimum length of output tokens | Median length of output tokens | 75th-percentile length of output tokens | 90th-percentile length of output tokens | 99th-percentile length of output tokens | Test data volume (from input parameters) |
| OutputTokenThroughput | Stage for this parameter | Average output throughput | Maximum output throughput | Minimum output throughput | Median output throughput | 75th-percentile output throughput | 90th-percentile output throughput | 99th-percentile output throughput | Test data volume (from input parameters) |
| PrefillTokenThroughput | Stage for this parameter | Average Prefill throughput | Maximum Prefill throughput | Minimum Prefill throughput | Median Prefill throughput | 75th-percentile Prefill throughput | 90th-percentile Prefill throughput | 99th-percentile Prefill throughput | Test data volume (from input parameters) |


## 2. End-to-End Performance Output Results
Expand All @@ -37,7 +39,6 @@ Explanations of key statistical indicators are as follows:
| **Max Concurrency** | Configured maximum concurrency |
| **Request Throughput** | Request-level throughput (requests per second, Requests/s) |
| **Total Input Tokens** | Total number of input tokens across all requests |
| **Prefill Token Throughput** | Token throughput during the Prefill stage (Token/s) |
| **Total Output Tokens** | Total number of output tokens generated across all requests |
| **Input Token Throughput** | Input token throughput (Token/s) |
| **Output Token Throughput** | Output token throughput (Token/s) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
+ InputTokens:请求的输入 Token 数量。
+ OutputTokens:请求生成的输出 Token 数量。
+ OutputTokenThroughput:输出 Token 的吞吐率(Token/s)。
+ PrefillTokenThroughput:Prefill 阶段的 Token 吞吐率(Token/s),按每个请求统计。
+ Tokenizer:Tokenizer 编码耗时。
+ Detokenizer:Detokenizer 解码耗时。

Expand All @@ -23,6 +24,7 @@
|InputTokens|统计此参数的阶段|输入token平均长度|最大输入token长度|最小输入token长度|输入token中位数长度|75分位输入token长度|90分位输入token长度|99分位输入token长度|测试数据量,来源于输入参数|
|OutputTokens|统计此参数的阶段|输出token平均长度|最大输出token长度|最小输出token长度|输出token中位数长度|75分位输出token长度|90分位输出token长度|99分位输出token长度|测试数据量,来源于输入参数|
|OutputTokenThroughput|统计此参数的阶段|平均输出吞吐|最大输出吞吐|最小输出吞吐|中位数输出吞吐|输出吞吐75分位|输出吞吐90分位|输出吞吐99分位|测试数据量,来源于输入参数|
|PrefillTokenThroughput|统计此参数的阶段|Prefill阶段平均吞吐率|Prefill阶段最大吞吐率|Prefill阶段最小吞吐率|Prefill阶段中位数吞吐率|75分位Prefill阶段吞吐率|90分位Prefill阶段吞吐率|99分位Prefill阶段吞吐率|测试数据量,来源于输入参数|

## 端到端性能输出结果
| 参数 | 说明 |
Expand All @@ -35,7 +37,6 @@
| **Max Concurrency** | 配置的最大并发数 |
| **Request Throughput** | 请求级吞吐率(请求数/秒) |
| **Total Input Tokens** | 所有请求的总输入 Token 数 |
| **Prefill Token Throughput** | Prefill 阶段的 Token 吞吐率 |
| **Total Output Tokens** | 所有请求生成的总输出 Token 数 |
| **Input Token Throughput** | 输入 Token 吞吐率 |
| **Output Token Throughput** | 输出 Token 吞吐率 |
Expand Down
209 changes: 209 additions & 0 deletions tests/UT/calculators/test_base_perf_metric_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,215 @@ def test_calc_common_metrics(self, mock_logger_class):

# 注意:不再测试零除情况,因为会导致异常

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_convert_result_prefill_token_throughput(self, mock_logger_class):
"""Test that convert_result calculates per-request PrefillTokenThroughput."""
mock_logger = MagicMock()
mock_logger_class.return_value = mock_logger
calculator = ConcretePerfMetricCalculator()

raw_result = {
'latency': [0.1, 0.2],
'ttft': [0.05, 0.10],
'tpot': [0.02, 0.03],
'input_tokens': [100, 200],
'output_tokens': [50, 60],
'generate_tokens_speed': [100, 200],
}
converted = calculator.convert_result(raw_result)

self.assertIn('PrefillTokenThroughput', converted)
self.assertEqual(len(converted['PrefillTokenThroughput']), 2)
# Prefill Throughput = input_tokens / ttft
self.assertAlmostEqual(converted['PrefillTokenThroughput'][0], 100 / 0.05)
self.assertAlmostEqual(converted['PrefillTokenThroughput'][1], 200 / 0.10)

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_convert_result_prefill_with_zero_ttft(self, mock_logger_class):
"""Test PrefillTokenThroughput when ttft is zero: appended as 0.0
but removed together with TTFT because TTFT sum is zero."""
mock_logger = MagicMock()
mock_logger_class.return_value = mock_logger
calculator = ConcretePerfMetricCalculator()

raw_result = {
'latency': [0.1],
'ttft': [0.0],
'tpot': [0.02], # non-zero tpot so only TTFT is removed
'input_tokens': [500],
'output_tokens': [50],
'generate_tokens_speed': [100],
}
converted = calculator.convert_result(raw_result)

# TTFT sum is 0.0 → both TTFT and PrefillTokenThroughput are removed
self.assertNotIn('TTFT', converted)
self.assertNotIn('PrefillTokenThroughput', converted)

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_convert_result_prefill_with_mixed_ttft(self, mock_logger_class):
"""Test PrefillTokenThroughput when some ttft values are zero."""
mock_logger = MagicMock()
mock_logger_class.return_value = mock_logger
calculator = ConcretePerfMetricCalculator()

raw_result = {
'latency': [0.1, 0.2],
'ttft': [0.0, 0.10],
'tpot': [0.02, 0.03],
'input_tokens': [100, 200],
'output_tokens': [50, 60],
'generate_tokens_speed': [100, 200],
}
converted = calculator.convert_result(raw_result)

# TTFT sum is NOT zero (0.0 + 0.10 = 0.10) → PrefillTokenThroughput retained
self.assertIn('TTFT', converted)
self.assertIn('PrefillTokenThroughput', converted)
self.assertEqual(len(converted['PrefillTokenThroughput']), 2)
# First request: ttft=0.0 → throughput=0.0
self.assertEqual(converted['PrefillTokenThroughput'][0], 0.0)
# Second request: ttft=0.10 → throughput=200/0.10=2000
self.assertAlmostEqual(converted['PrefillTokenThroughput'][1], 200 / 0.10)

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_convert_result_prefill_with_list_input_tokens(self, mock_logger_class):
"""Test PrefillTokenThroughput with list-type input_tokens (summed)."""
mock_logger = MagicMock()
mock_logger_class.return_value = mock_logger
calculator = ConcretePerfMetricCalculator()

raw_result = {
'latency': [0.1],
'ttft': [0.05],
'input_tokens': [[100, 200, 300]], # list of lists
'output_tokens': [50],
'generate_tokens_speed': [100],
}
converted = calculator.convert_result(raw_result)

self.assertIn('PrefillTokenThroughput', converted)
# Should sum the list: 100 + 200 + 300 = 600, then 600 / 0.05 = 12000
self.assertAlmostEqual(converted['PrefillTokenThroughput'][0], 600 / 0.05)

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_convert_result_prefill_removed_when_ttft_zero(self, mock_logger_class):
"""Test PrefillTokenThroughput is removed when TTFT sum is zero."""
mock_logger = MagicMock()
mock_logger_class.return_value = mock_logger
calculator = ConcretePerfMetricCalculator()

raw_result = {
'latency': [0.1, 0.2],
'ttft': [0.0, 0.0],
'tpot': [0.02, 0.03],
'input_tokens': [100, 200],
'output_tokens': [50, 60],
'generate_tokens_speed': [100, 200],
}
converted = calculator.convert_result(raw_result)

# TTFT should be removed (sum is 0.0)
self.assertNotIn('TTFT', converted)
# PrefillTokenThroughput should also be removed when TTFT is removed
self.assertNotIn('PrefillTokenThroughput', converted)

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_convert_result_no_prefill_without_ttft(self, mock_logger_class):
"""Test PrefillTokenThroughput is not added when no ttft/input_tokens."""
mock_logger = MagicMock()
mock_logger_class.return_value = mock_logger
calculator = ConcretePerfMetricCalculator()

raw_result = {
'latency': [0.1],
'output_tokens': [50],
'generate_tokens_speed': [100],
}
converted = calculator.convert_result(raw_result)

# No input_tokens or ttft -> PrefillTokenThroughput should not be present
self.assertNotIn('PrefillTokenThroughput', converted)

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_add_units_to_metrics_prefill_after_output(self, mock_logger_class):
"""Test PrefillTokenThroughput appears after OutputTokenThroughput in units map."""
mock_logger = MagicMock()
mock_logger_class.return_value = mock_logger
calculator = ConcretePerfMetricCalculator()

metrics = {
'OutputTokenThroughput': {
'stage1': {'Average': 100.0, 'Min': 50.0, 'Max': 150.0, 'N': 10}
},
'PrefillTokenThroughput': {
'stage1': {'Average': 200.0, 'Min': 100.0, 'Max': 300.0, 'N': 10}
},
}

metrics_with_units = calculator._add_units_to_metrics(metrics)

self.assertEqual(
metrics_with_units['OutputTokenThroughput']['stage1']['Average'],
'100.0 token/s'
)
self.assertEqual(
metrics_with_units['PrefillTokenThroughput']['stage1']['Average'],
'200.0 token/s'
)

# Verify the order: OutputTokenThroughput key should come before PrefillTokenThroughput
keys = list(metrics_with_units.keys())
output_idx = keys.index('OutputTokenThroughput')
prefill_idx = keys.index('PrefillTokenThroughput')
self.assertLess(output_idx, prefill_idx,
'OutputTokenThroughput should be before PrefillTokenThroughput')

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_calc_common_metrics_no_prefill_token_throughput(self, mock_logger_class):
"""Test Prefill Token Throughput is NOT in common metrics (moved to per-request)."""
mock_logger = MagicMock()
mock_logger_class.return_value = mock_logger
simple_perf_details = {
'stage_dict': {'stage1': {}},
'infer_time': {'stage1': 10.0},
'data_count': {'stage1': 100},
'success_count': {'stage1': 95},
'result': {
'stage1': {
'E2EL': [0.1, 0.2, 0.3],
'TTFT': [0.05, 0.06, 0.07],
'InputTokens': [10, 20, 30],
'OutputTokens': [50, 60, 70]
}
},
'decode_latencies': {
'stage1': [[0.02], [0.03], [0.04]]
}
}
calculator = ConcretePerfMetricCalculator()
calculator._init_datas(simple_perf_details, 10)
calculator._calc_common_metrics()

# Prefill Token Throughput should NOT be in common metrics
self.assertNotIn('Prefill Token Throughput', calculator.common_metrics)
self.assertIn('Output Token Throughput', calculator.common_metrics)

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_add_units_to_common_metrics_no_prefill_token_throughput(self, mock_logger_class):
"""Test Prefill Token Throughput is NOT in common metrics units map."""
mock_logger = MagicMock()
mock_logger_class.return_value = mock_logger
calculator = ConcretePerfMetricCalculator()

common_metrics = {
'Output Token Throughput': {'stage1': 100.5},
}

result = calculator._add_units_to_common_metrics(common_metrics)
# Prefill Token Throughput should not have a unit mapping
self.assertEqual(result['Output Token Throughput']['stage1'], '100.5 token/s')

@patch('ais_bench.benchmark.calculators.base_perf_metric_calculator.AISLogger')
def test_calculate_concurrency(self, mock_logger_class):
# 测试并发计算
Expand Down
Loading