Skip to content

Commit 8bb9316

Browse files
authored
Add quantization timing metrics to YAML benchmarking (#636)
* Added index construction metrics to logging. * Add quantization timing metrics, prefix selection, and logging schema support. * Denoised console output. * Refactored overlapping getCompressor overloads into a unified implementation with helpers.
1 parent cf0abeb commit 8bb9316

12 files changed

Lines changed: 580 additions & 125 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ local/
88
pq_cache/
99
index_cache/
1010

11+
### Logging (or whatever you use)
12+
logging/
13+
1114
### JVM crashes
1215
hs_err_pid*
1316
replay_pid*

jvector-examples/src/main/java/io/github/jbellis/jvector/example/Grid.java

Lines changed: 282 additions & 63 deletions
Large diffs are not rendered by default.

jvector-examples/src/main/java/io/github/jbellis/jvector/example/benchmarks/BenchmarkTablePrinter.java

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,39 +29,59 @@
2929
public class BenchmarkTablePrinter {
3030
private static final int MIN_COLUMN_WIDTH = 11;
3131
private static final int MIN_HEADER_PADDING = 3;
32+
private static final int MAX_COLUMN_WIDTH = 15; // tune as desired
3233

3334
private String headerFmt;
3435
private String rowFmt;
36+
private int[] colWidths;
3537

3638
public BenchmarkTablePrinter() {
3739
headerFmt = null;
3840
rowFmt = null;
3941
}
4042

43+
/**
44+
* Clears header/row formats so the next printed row will emit a fresh header.
45+
* Call this when the set of columns may change (e.g., when topK changes).
46+
*/
47+
public void resetTable() {
48+
headerFmt = null;
49+
rowFmt = null;
50+
colWidths = null;
51+
}
4152

4253
private void initializeHeader(List<Metric> cols) {
4354
if (headerFmt != null) {
4455
return;
4556
}
57+
this.colWidths = new int[cols.size() + 1];
4658

4759
// Build the format strings for header & rows
4860
StringBuilder hsb = new StringBuilder();
4961
StringBuilder rsb = new StringBuilder();
5062

5163
// 1) Overquery column width
64+
// Overquery column width
5265
hsb.append("%-12s");
5366
rsb.append("%-12.2f");
67+
colWidths[0] = 12;
5468

5569
// 2) One column per Metric
70+
int i = 0;
5671
for (Metric m : cols) {
5772
String hdr = m.getHeader();
5873
String spec = m.getFmtSpec();
5974
int width = Math.max(MIN_COLUMN_WIDTH, hdr.length() + MIN_HEADER_PADDING);
75+
width = Math.min(width, MAX_COLUMN_WIDTH);
76+
77+
colWidths[i + 1] = width;
6078

6179
// Header: Always a string
6280
hsb.append(" %-").append(width).append("s");
6381
// Row: Use the Metric’s fmtSpec (e.g. ".2f", ".3f")
6482
rsb.append(" %-").append(width).append(spec);
83+
84+
i++;
6585
}
6686

6787
this.headerFmt = hsb.toString();
@@ -72,33 +92,55 @@ private void initializeHeader(List<Metric> cols) {
7292
}
7393

7494
/**
75-
* Call this once to print all the global parameters before the table.
95+
* Prints the run-wide configuration header (index settings followed by query settings)
96+
* once per run, before any results table output.
7697
*
77-
* @param params a map from parameter name (e.g. "mGrid") to its List value
98+
* Iteration order is preserved (use an insertion-ordered map such as {@link java.util.LinkedHashMap}).
99+
*
100+
* @param indexParams ordered map of index-construction parameters to print
101+
* @param queryParams ordered map of query/search parameters to print
78102
*/
79-
public void printConfig(Map<String, ?> params) {
80-
System.out.println();
81-
System.out.println("Configuration:");
82-
params.forEach((name, values) ->
83-
System.out.printf(Locale.US, " %-22s: %s%n", name, values)
84-
);
103+
public void printConfig(Map<String, ?> indexParams, Map<String, ?> queryParams) {
104+
printSection("\nIndex configuration", indexParams);
105+
printSection("\nQuery configuration", queryParams);
106+
}
107+
108+
private void printSection(String title, Map<String, ?> params) {
109+
System.out.println(title + ":");
110+
for (var e : params.entrySet()) {
111+
System.out.printf(" %-20s %s%n", e.getKey(), String.valueOf(e.getValue()));
112+
}
85113
}
86114

87115
private void printHeader(List<Metric> cols) {
88-
// Prepare array: First "Overquery", then each Metric header
89-
Object[] hdrs = new Object[cols.size() + 1];
90-
hdrs[0] = "Overquery";
116+
// Two header lines: split long headers onto a second line when possible
117+
Object[] hdrs1 = new Object[cols.size() + 1];
118+
Object[] hdrs2 = new Object[cols.size() + 1];
119+
120+
hdrs1[0] = "Overquery";
121+
hdrs2[0] = "";
122+
123+
boolean anySecondLine = false;
124+
91125
for (int i = 0; i < cols.size(); i++) {
92-
hdrs[i + 1] = cols.get(i).getHeader();
126+
String hdr = cols.get(i).getHeader();
127+
int width = colWidths[i + 1];
128+
129+
String[] parts = splitHeader2(hdr, width);
130+
hdrs1[i + 1] = parts[0];
131+
hdrs2[i + 1] = parts[1];
132+
if (!parts[1].isEmpty()) anySecondLine = true;
93133
}
94134

95-
// Print header line
96-
String line = String.format(Locale.US, headerFmt, hdrs);
97-
System.out.println(line);
98-
// Underline of same length
99-
System.out.println(String.join("",
100-
Collections.nCopies(line.length(), "-")
101-
));
135+
String line1 = String.format(Locale.US, headerFmt, hdrs1);
136+
System.out.println(line1);
137+
138+
if (anySecondLine) {
139+
String line2 = String.format(Locale.US, headerFmt, hdrs2);
140+
System.out.println(line2);
141+
}
142+
143+
System.out.println(String.join("", Collections.nCopies(line1.length(), "-")));
102144
}
103145

104146
/**
@@ -109,7 +151,7 @@ private void printHeader(List<Metric> cols) {
109151
*/
110152
public void printRow(double overquery,
111153
List<Metric> cols) {
112-
initializeHeader(cols);
154+
initializeHeader(cols); // lazy: prints header on the first row after resetTable()
113155

114156
// Build argument array: First overquery, then each Metric.extract(...)
115157
Object[] vals = new Object[cols.size() + 1];
@@ -129,4 +171,31 @@ public void printRow(double overquery,
129171
public void printFooter() {
130172
System.out.println();
131173
}
174+
175+
// Helper for splitting the header into two rows
176+
private static String[] splitHeader2(String hdr, int colWidth) {
177+
if (hdr == null) return new String[] { "", "" };
178+
179+
// Manual break: "Line1\nLine2"
180+
int nl = hdr.indexOf('\n');
181+
if (nl >= 0) {
182+
String a = hdr.substring(0, nl).trim();
183+
String b = hdr.substring(nl + 1).trim();
184+
return new String[] { a, b };
185+
}
186+
187+
// Leave a little slack for padding/alignment
188+
int max = Math.max(1, colWidth - MIN_HEADER_PADDING);
189+
190+
String s = hdr.trim();
191+
if (s.length() <= max) return new String[] { s, "" };
192+
193+
// Find last space before max; if none, hard-split
194+
int cut = s.lastIndexOf(' ', max);
195+
if (cut <= 0) cut = max;
196+
197+
String a = s.substring(0, cut).trim();
198+
String b = s.substring(cut).trim();
199+
return new String[] { a, b };
200+
}
132201
}

jvector-examples/src/main/java/io/github/jbellis/jvector/example/benchmarks/Metric.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ public static Metric of(String key, String header, String fmtSpec, double value)
5050

5151
@Override
5252
public String toString() {
53-
return String.format(header + " = " + fmtSpec, value);
53+
// Create a safe template: "%s = %" + fmtSpec
54+
String template = "%s = %" + fmtSpec;
55+
56+
// Pass 'header' as the first argument (%s) and 'value' as the second argument (%.1f)
57+
return String.format(template, header, value);
5458
}
5559
}

jvector-examples/src/main/java/io/github/jbellis/jvector/example/benchmarks/QueryTester.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,25 @@ public List<Metric> run(
102102
var diskSnapshot = diagnostics.getLatestDiskSnapshot();
103103

104104
if (systemSnapshot != null) {
105-
results.add(Metric.of("search.system.max_heap_mb", "Max heap usage", ".1f",
105+
// Max heap usage in MB
106+
results.add(Metric.of("search.system.max_heap_mb", "Max heap usage (MB)", ".1f",
106107
systemSnapshot.memoryStats.heapUsed / (1024.0 * 1024.0)));
107108

108-
results.add(Metric.of("search.system.max_offheap_mb", "Max offheap usage", ".1f",
109+
// Max off-heap usage (direct + mapped) in MB
110+
results.add(Metric.of("search.system.max_offheap_mb", "Max offheap usage (MB)", ".1f",
109111
systemSnapshot.memoryStats.getTotalOffHeapMemory() / (1024.0 * 1024.0)));
110112
}
111113

114+
if (diskSnapshot != null) {
115+
// Number of index files created
116+
results.add(Metric.of("search.disk.file_count", "File count", ".0f",
117+
diskSnapshot.fileCount));
118+
119+
// Total size of index files created
120+
results.add(Metric.of("search.disk.total_file_size_mb", "Total file size (MB)", ".1f",
121+
diskSnapshot.totalBytes / (1024.0 * 1024.0)));
122+
}
123+
112124
} catch (IOException e) {
113125
throw new RuntimeException(e);
114126
}

jvector-examples/src/main/java/io/github/jbellis/jvector/example/benchmarks/ThroughputBenchmark.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ public List<Metric> runBenchmark(
257257
var list = new ArrayList<Metric>();
258258
if (computeAvgQps) {
259259
list.add(Metric.of("search.throughput.avg_qps",
260-
"Avg QPS (of " + numTestRuns + ")",
260+
"Avg QPS\n (of " + numTestRuns + ")",
261261
formatAvgQps,
262262
avgQps));
263263

@@ -273,18 +273,18 @@ public List<Metric> runBenchmark(
273273
}
274274
if (computeMedianQps) {
275275
list.add(Metric.of("search.throughput.median_qps",
276-
"Median QPS (of " + numTestRuns + ")",
276+
"Median QPS\n (of " + numTestRuns + ")",
277277
formatMedianQps,
278278
medianQps));
279279
}
280280
if (computeMaxQps) {
281281
list.add(Metric.of("search.throughput.max_qps",
282-
"Max QPS (of " + numTestRuns + ")",
282+
"Max QPS\n (of " + numTestRuns + ")",
283283
formatMaxQps,
284284
maxQps));
285285

286286
list.add(Metric.of("search.throughput.min_qps",
287-
"Min QPS (of " + numTestRuns + ")",
287+
"Min QPS\n (of " + numTestRuns + ")",
288288
formatMaxQps,
289289
minQps));
290290
}

jvector-examples/src/main/java/io/github/jbellis/jvector/example/reporting/LoggingSchemaPlanner.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public static List<String> unionLoggingMetricKeys(RunConfig runCfg, List<MultiCo
5656
var ctx = ReportingSelectionResolver.Context.of("topK", Integer.toString(topK));
5757
var resolved = ReportingSelectionResolver.resolve(runCfg.logging, SearchReportingCatalog.catalog(), ctx);
5858
union.addAll(resolved.keys());
59+
expandPrefixes(union, resolved.prefixes(), allConfigs);
5960
}
6061

6162
// 2) Emit keys in canonical order, only if present in union
@@ -110,4 +111,61 @@ private static void addIfPresent(Set<String> union, List<String> ordered, String
110111
ordered.add(key);
111112
}
112113
}
114+
115+
private static void expandPrefixes(Set<String> union,
116+
Set<String> prefixes,
117+
List<MultiConfig> allConfigs) {
118+
if (prefixes == null || prefixes.isEmpty()) return;
119+
120+
// Collect quant types from YAML configs (domain is defined by yaml `compression.type` and `reranking`)
121+
Set<String> indexQuantTypes = new HashSet<>();
122+
Set<String> searchQuantTypes = new HashSet<>();
123+
boolean wantsNVQ = false;
124+
125+
for (MultiConfig cfg : allConfigs) {
126+
if (cfg == null) continue;
127+
128+
// construction compression types (PQ/BQ/None)
129+
if (cfg.construction != null && cfg.construction.compression != null) {
130+
for (var c : cfg.construction.compression) {
131+
if (c != null && c.type != null && !"None".equals(c.type)) {
132+
indexQuantTypes.add(c.type); // "PQ" or "BQ"
133+
}
134+
}
135+
}
136+
137+
// construction reranking types (FP/NVQ)
138+
if (cfg.construction != null && cfg.construction.reranking != null) {
139+
if (cfg.construction.reranking.contains("NVQ")) {
140+
wantsNVQ = true;
141+
}
142+
}
143+
144+
// search compression types (PQ/BQ/None)
145+
if (cfg.search != null && cfg.search.compression != null) {
146+
for (var c : cfg.search.compression) {
147+
if (c != null && c.type != null && !"None".equals(c.type)) {
148+
searchQuantTypes.add(c.type); // "PQ" or "BQ"
149+
}
150+
}
151+
}
152+
}
153+
154+
for (String p : prefixes) {
155+
if ("construction.index_quant_time_s".equals(p)) {
156+
for (String qt : indexQuantTypes) {
157+
union.add(p + "." + qt + ".compute_time_s");
158+
union.add(p + "." + qt + ".encoding_time_s");
159+
}
160+
if (wantsNVQ) {
161+
union.add(p + ".NVQ.compute_time_s"); // we intentionally do not time NVQ encode
162+
}
163+
} else if ("search.search_quant_time_s".equals(p)) {
164+
for (String qt : searchQuantTypes) {
165+
union.add(p + "." + qt + ".compute_time_s");
166+
union.add(p + "." + qt + ".encoding_time_s");
167+
}
168+
}
169+
}
170+
}
113171
}

0 commit comments

Comments
 (0)