Skip to content

Commit 6a1d02a

Browse files
polarGHongtao Zhang
andauthored
Enhancement - Fix hipblaslt-gemm result parsing for hipBLASLt v1500+ output format (#791)
**Description** The hipblaslt-gemm benchmark result parser fails with MICROBENCHMARK_RESULT_PARSING_FAILURE (return code 33) when running against hipBLASLt v1500+. The benchmark kernels execute successfully and produce valid TFLOPS data, but SuperBench cannot parse the results into structured metrics. Root cause: The parser hardcodes len(fields) != 23 to validate the output CSV, but hipBLASLt v1500 outputs 34 columns — it added a_type, b_type, c_type, d_type, scaleA, scaleB, scaleC, scaleD, amaxD, bias_type, aux_type, and hipblaslt-GB/s. This causes two bugs: 1. The field count check rejects every result line as invalid. 2. Even if it passed, fields[-2] would return hipblaslt-GB/s instead of hipblaslt-Gflops. **Fix** Replace the hardcoded field-count check and positional index with header-based column lookup: - Parse the CSV header line to dynamically find the column index of hipblaslt-Gflops. - Validate that the data line has the same number of columns as the header (instead of a magic number). - Use the discovered column index to extract the Gflops value. This approach is: - Backward compatible — works with the old 23-column format (hipBLASLt v600). - Forward compatible — will handle any future column additions as long as hipblaslt-Gflops remains in the header. --------- Co-authored-by: Hongtao Zhang <hongtaozhang@microsoft.com>
1 parent 932d9f6 commit 6a1d02a

2 files changed

Lines changed: 132 additions & 10 deletions

File tree

superbench/benchmarks/micro_benchmarks/hipblaslt_function.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""Module of the hipBlasLt GEMM benchmark."""
55

66
import os
7+
import re
78

89
from superbench.common.utils import logger
910
from superbench.benchmarks import BenchmarkRegistry, Platform, ReturnCode
@@ -110,7 +111,7 @@ def _process_raw_result(self, cmd_idx, raw_output):
110111
lines = raw_output.splitlines()
111112
index = None
112113

113-
# Find the line containing 'hipblaslt-Gflops'
114+
# Find the header line containing 'hipblaslt-Gflops'
114115
for i, line in enumerate(lines):
115116
if 'hipblaslt-Gflops' in line:
116117
index = i
@@ -119,16 +120,53 @@ def _process_raw_result(self, cmd_idx, raw_output):
119120
if index is None:
120121
raise ValueError('Line with "hipblaslt-Gflops" not found in the log.')
121122

122-
# Split the line into fields using a comma as the delimiter
123+
# Parse the header and resolve every key column (batch_count/m/n/k/hipblaslt-Gflops)
124+
# by name. This keeps the parser forward-compatible across known and future
125+
# hipBLASLt output formats (v600: 23 columns; v1500: 34 columns with extra
126+
# a_type/b_type/c_type/scaleA-D/amaxD/bias_type/aux_type/hipblaslt-GB/s),
127+
# without relying on any fixed column position.
128+
header_fields = lines[index].strip().split(',')
129+
# Strip leading rank markers like '[0]' or '[0]:' from the first header field.
130+
# Use a regex anchored at the start so a column name that legitimately contains
131+
# ']' (unlikely, but defensive) is not truncated.
132+
header_fields[0] = re.sub(r'^\s*\[\d+\]:?', '', header_fields[0])
133+
134+
# Build a name -> column-index map (first occurrence wins for any duplicates).
135+
col_idx_by_name = {}
136+
for col_idx, col_name in enumerate(header_fields):
137+
col_idx_by_name.setdefault(col_name.strip(), col_idx)
138+
139+
required_columns = ['batch_count', 'm', 'n', 'k', 'hipblaslt-Gflops']
140+
missing_columns = [c for c in required_columns if c not in col_idx_by_name]
141+
if missing_columns:
142+
raise ValueError(f'Required column(s) not found in header: {missing_columns}.')
143+
144+
# Ensure a data line follows the header (e.g., hipblaslt-bench may have
145+
# crashed after printing the header).
146+
if index + 1 >= len(lines):
147+
raise ValueError('Data line missing after "hipblaslt-Gflops" header.')
148+
149+
# Split the data line into fields using a comma as the delimiter
123150
fields = lines[index + 1].strip().split(',')
124151

125-
# Check the number of fields and the format of the first two fields
126-
if len(fields) != 23:
127-
raise ValueError('Invalid result')
152+
# Validate that the data line has the same number of columns as the header
153+
if len(fields) != len(header_fields):
154+
raise ValueError(
155+
f'Field count mismatch: header has {len(header_fields)} columns '
156+
f'but data has {len(fields)} columns'
157+
)
158+
159+
# Resolve every key value by header name and strip whitespace from each, so
160+
# any padding around CSV values does not bleed into the metric key.
161+
batch_count = fields[col_idx_by_name['batch_count']].strip()
162+
m_val = fields[col_idx_by_name['m']].strip()
163+
n_val = fields[col_idx_by_name['n']].strip()
164+
k_val = fields[col_idx_by_name['k']].strip()
165+
gflops_col = col_idx_by_name['hipblaslt-Gflops']
128166

129167
self._result.add_result(
130-
f'{self._precision_in_commands[cmd_idx]}_{fields[3]}_{"_".join(fields[4:7])}_flops',
131-
float(fields[-2]) / 1000
168+
f'{self._precision_in_commands[cmd_idx]}_{batch_count}_{m_val}_{n_val}_{k_val}_flops',
169+
float(fields[gflops_col]) / 1000
132170
)
133171
except BaseException as e:
134172
self._result.set_return_code(ReturnCode.MICROBENCHMARK_RESULT_PARSING_FAILURE)

tests/benchmarks/micro_benchmarks/test_hipblaslt_function.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def test_hipblaslt_gemm_result_parsing(self):
8787
benchmark._args = SimpleNamespace(shapes=['896,896,896'], in_types=['fp16'], log_raw_data=False)
8888
benchmark._result = BenchmarkResult(self.benchmark_name, BenchmarkType.MICRO, ReturnCode.SUCCESS, run_count=1)
8989

90+
# Old format (hipBLASLt v600, 23 columns)
9091
example_raw_output = """
9192
hipBLASLt version: 600
9293
hipBLASLt git version: 52776da
@@ -101,12 +102,95 @@ def test_hipblaslt_gemm_result_parsing(self):
101102
[0]transA,transB,grouped_gemm,batch_count,m,n,k,alpha,lda,stride_a,beta,ldb,stride_b,ldc,stride_c,ldd,stride_d,d_type,compute_type,activation_type,bias_vector,hipblaslt-Gflops,us
102103
N,N,0,1,896,896,896,1,896,802816,0,896,802816,896,802816,896,802816,fp16_r,f32_r,none,0, 58624.5, 24.54
103104
"""
104-
# Positive case - valid raw output
105+
# Positive case - valid raw output (old format)
105106
self.assertTrue(benchmark._process_raw_result(0, example_raw_output))
106107
self.assertEqual(ReturnCode.SUCCESS, benchmark.return_code)
107108

108-
self.assertEqual(2, len(benchmark.result))
109-
self.assertEqual(58.6245, benchmark.result['fp16_1_896_896_896_flops'][0])
109+
self.assertIn('fp16_1_896_896_896_flops', benchmark.result)
110+
self.assertAlmostEqual(58.6245, benchmark.result['fp16_1_896_896_896_flops'][0], places=4)
110111

111112
# Negative case - invalid raw output
112113
self.assertFalse(benchmark._process_raw_result(1, 'HipBLAS API failed'))
114+
115+
def test_hipblaslt_gemm_result_parsing_new_format(self):
116+
"""Test hipblaslt-bench benchmark result parsing with new 34-column format (hipBLASLt v1500+)."""
117+
benchmark = self.get_benchmark()
118+
self.assertTrue(benchmark._preprocess())
119+
benchmark._args = SimpleNamespace(shapes=['4096,4096,4096'], in_types=['fp16'], log_raw_data=False)
120+
benchmark._result = BenchmarkResult(self.benchmark_name, BenchmarkType.MICRO, ReturnCode.SUCCESS, run_count=1)
121+
122+
# New format (hipBLASLt v1500, 34 columns) - includes a_type, b_type, c_type, d_type,
123+
# scaleA, scaleB, scaleC, scaleD, amaxD, bias_type, aux_type, and hipblaslt-GB/s columns
124+
example_raw_output_new = """
125+
hipBLASLt version: 1500
126+
hipBLASLt git version: 8c69191d
127+
Query device success: there are 1 devices. (Target device ID is 0)
128+
Device ID 0 : gfx942:sramecc+:xnack-
129+
with 205.6 GB memory, max. SCLK 2100 MHz, max. MCLK 1300 MHz, compute capability 9.4
130+
maxGridDimX 2147483647, sharedMemPerBlock 65.5 KB, maxThreadsPerBlock 1024, warpSize 64
131+
132+
Is supported 1 / Total solutions: 1
133+
[0]:transA,transB,grouped_gemm,batch_count,m,n,k,alpha,lda,stride_a,beta,ldb,stride_b,ldc,stride_c,ldd,stride_d,a_type,b_type,c_type,d_type,compute_type,scaleA,scaleB,scaleC,scaleD,amaxD,activation_type,bias_vector,bias_type,aux_type,hipblaslt-Gflops,hipblaslt-GB/s,us
134+
N,N,0,1,4096,4096,4096,1,4096,16777216,0,4096,16777216,4096,16777216,4096,16777216,f16_r,f16_r,f16_r,f16_r,f32_r,0,0,0,0,0,none,0,f16_r,f16_r,678209,462.62,202.65
135+
"""
136+
# Positive case - valid raw output (new format)
137+
self.assertTrue(benchmark._process_raw_result(0, example_raw_output_new))
138+
self.assertEqual(ReturnCode.SUCCESS, benchmark.return_code)
139+
140+
self.assertIn('fp16_1_4096_4096_4096_flops', benchmark.result)
141+
self.assertAlmostEqual(678.209, benchmark.result['fp16_1_4096_4096_4096_flops'][0], places=3)
142+
143+
def test_hipblaslt_gemm_result_parsing_future_format_with_inserted_column(self):
144+
"""Test that the parser is forward-compatible when a new column is inserted before batch_count.
145+
146+
This proves the metric key is built purely from header-named columns, not fixed
147+
positions, so reordering or inserting columns in a future hipBLASLt release does
148+
not silently produce a wrong metric key.
149+
"""
150+
benchmark = self.get_benchmark()
151+
self.assertTrue(benchmark._preprocess())
152+
benchmark._args = SimpleNamespace(shapes=['4096,4096,4096'], in_types=['fp16'], log_raw_data=False)
153+
benchmark._result = BenchmarkResult(self.benchmark_name, BenchmarkType.MICRO, ReturnCode.SUCCESS, run_count=1)
154+
155+
# Synthetic future format: a new column 'fake_new_col' is inserted before batch_count,
156+
# and the data values for the key fields are padded with whitespace to confirm that
157+
# individual field values are stripped before being used to build the metric key.
158+
# A minimal header is used so the padded data line stays within the 120-column limit.
159+
example_raw_output_future = """
160+
hipBLASLt version: 9999
161+
Is supported 1 / Total solutions: 1
162+
[0]:transA,transB,fake_new_col,batch_count,m,n,k,hipblaslt-Gflops,us
163+
N,N,FAKE, 1 , 4096 , 4096 , 4096 ,678209,202.65
164+
"""
165+
self.assertTrue(benchmark._process_raw_result(0, example_raw_output_future))
166+
self.assertEqual(ReturnCode.SUCCESS, benchmark.return_code)
167+
168+
# The correct, header-driven key must be present with the correct value.
169+
self.assertIn('fp16_1_4096_4096_4096_flops', benchmark.result)
170+
self.assertAlmostEqual(678.209, benchmark.result['fp16_1_4096_4096_4096_flops'][0], places=3)
171+
172+
# No key derived from the wrong (positional) field should leak through.
173+
for key in benchmark.result:
174+
self.assertNotIn('FAKE', key)
175+
self.assertNotIn('fake_new_col', key)
176+
177+
def test_hipblaslt_gemm_result_parsing_missing_required_column(self):
178+
"""Test that the parser fails loudly when a required key column (e.g. batch_count) is missing.
179+
180+
Failing surfaces unknown output formats explicitly instead of silently producing
181+
a wrong metric key.
182+
"""
183+
benchmark = self.get_benchmark()
184+
self.assertTrue(benchmark._preprocess())
185+
benchmark._args = SimpleNamespace(shapes=['896,896,896'], in_types=['fp16'], log_raw_data=False)
186+
benchmark._result = BenchmarkResult(self.benchmark_name, BenchmarkType.MICRO, ReturnCode.SUCCESS, run_count=1)
187+
188+
# batch_count is removed from both the header and the data line.
189+
example_raw_output_missing_col = """
190+
hipBLASLt version: 600
191+
Is supported 1 / Total solutions: 1
192+
[0]transA,transB,grouped_gemm,m,n,k,alpha,lda,stride_a,beta,ldb,stride_b,ldc,stride_c,ldd,stride_d,d_type,compute_type,activation_type,bias_vector,hipblaslt-Gflops,us
193+
N,N,0,896,896,896,1,896,802816,0,896,802816,896,802816,896,802816,fp16_r,f32_r,none,0, 58624.5, 24.54
194+
"""
195+
self.assertFalse(benchmark._process_raw_result(0, example_raw_output_missing_col))
196+
self.assertEqual(ReturnCode.MICROBENCHMARK_RESULT_PARSING_FAILURE, benchmark.return_code)

0 commit comments

Comments
 (0)