lib/accy/src/profiling/publication/suite.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const accy = @import("accy");
3 const bench = @import("bench");
4 const choir = @import("choir");
5 const sys = @import("sys");
6 const namespace = @import("root.zig");
7
8 const coz = bench.coz;
9 const floor = bench.floor;
10 const perf = sys.perf;
11 const floor_mod = namespace.floor;
12 const options_mod = namespace.options;
13 const operation = choir.product.operation;
14 const revision = choir.product.revision;
15 const publication = accy.preparation.publication;
16 const Prepared = accy.preparation.pipeline.BackendPreparedModule;
17 const PublicationStage = accy.choir.publication.Stage;
18
19 const stage_count = floor_mod.stage_count;
20 const maximum_samples = options_mod.maximum_samples;
21 const record_bytes: u32 = 32 * 1024 * 1024;
22 const workspace_bytes: u64 = 3328 * 1024 * 1024;
23 const work_events: u32 = 256;
24 const chain_elements: i64 = 4096;
25 const chain_length: u32 = 64;
26 const gap_ratio: u64 = 10;
27
28 const context_limits = blk: {
29 var limits = choir.ir.Context.Limits.testing;
30 limits.operations.storage_bytes = 1024 * 1024;
31 limits.operations.nested_bytes = 1024 * 1024;
32 break :blk limits;
33 };
34
35 const configuration = operation.Configuration{
36 .context = context_limits,
37 .register = registerContext,
38 .registration = .{ .name = "accy-publication-bench", .version = 1 },
39 .codec = .{ .operations = 10000, .entities = 10000, .fields = 10000, .depth = 64 },
40 .image = .{ .bytes = 16 * 1024 * 1024, .entities = 10000, .depth = 64 },
41 .roots = 1,
42 .gate_scratch = 128 * 1024 * 1024,
43 .verify = choir.ir.verify.default_options,
44 };
45
46 const hardware_selectors = [_]perf.CounterSelector{.{ .hardware = .cycles }};
47 const software_selectors = [_]perf.CounterSelector{
48 .{ .software = .page_faults },
49 .{ .software = .task_clock },
50 };
51 const counter_options = perf.CounterOptions{ .exclude_kernel = true, .exclude_hv = true };
52 const HardwareRegion = perf.CounterSetRegion(hardware_selectors.len);
53 const SoftwareRegion = perf.CounterSetRegion(software_selectors.len);
54
55 const Interval = struct {
56 ns: u64,
57 cycles: ?u64 = null,
58 page_faults: ?u64 = null,
59 task_ns: ?u64 = null,
60 };
61
62 /// Elapsed time plus counter readings for a single timed region: page faults
63 /// and task clock from software counters, and cycles from a hardware counter.
64 /// The benchmark wraps each timed operation in one of these. The hardware
65 /// counter opens after the software ones and closes before them, so the cycle
66 /// count includes as little counter bookkeeping as possible.
67 const Timer = struct {
68 hardware: ?HardwareRegion,
69 software: ?SoftwareRegion,
70 start: i128,
71
72 fn begin() Timer {
73 const software = SoftwareRegion.start(&software_selectors, counter_options) catch null;
74 const hardware = HardwareRegion.start(&hardware_selectors, counter_options) catch null;
75 return .{ .hardware = hardware, .software = software, .start = bench.nowNs() };
76 }
77
78 fn end(self: *Timer) Interval {
79 var interval = Interval{ .ns = bench.elapsedNs(self.start) };
80 if (self.hardware) |*region| {
81 defer region.deinit();
82 if (region.stop() catch null) |counts| {
83 interval.cycles = complete(counts.find(hardware_selectors[0]));
84 }
85 }
86 if (self.software) |*region| {
87 defer region.deinit();
88 if (region.stop() catch null) |counts| {
89 interval.page_faults = complete(counts.find(software_selectors[0]));
90 interval.task_ns = complete(counts.find(software_selectors[1]));
91 }
92 }
93 self.hardware = null;
94 self.software = null;
95 return interval;
96 }
97 };
98
99 fn complete(count: ?perf.CountResult) ?u64 {
100 const value = count orelse return null;
101 if (value.runningCoverage() != .complete) return null;
102 return value.value;
103 }
104
105 const Series = struct {
106 ns: [maximum_samples]u64 = undefined,
107 page_faults: [maximum_samples]u64 = undefined,
108 task_ns: [maximum_samples]u64 = undefined,
109 len: u8 = 0,
110 cycles: u64 = 0,
111 elapsed_ns: u64 = 0,
112 cycles_counted: bool = true,
113 software_counted: bool = true,
114
115 fn record(self: *Series, interval: Interval) void {
116 std.debug.assert(self.len < maximum_samples);
117 std.debug.assert(interval.ns > 0);
118 self.ns[self.len] = interval.ns;
119 self.page_faults[self.len] = interval.page_faults orelse 0;
120 self.task_ns[self.len] = interval.task_ns orelse 0;
121 self.len += 1;
122 self.elapsed_ns += interval.ns;
123 if (interval.cycles) |cycles| self.cycles += cycles else self.cycles_counted = false;
124 if (interval.page_faults == null) self.software_counted = false;
125 if (interval.task_ns == null) self.software_counted = false;
126 }
127
128 fn clock(self: *const Series) ?floor.Clock {
129 if (!self.cycles_counted) return null;
130 return floor.Clock.observed(self.cycles, self.elapsed_ns) catch null;
131 }
132 };
133
134 /// Returns the lower median of `values`, the smaller of the two middle samples,
135 /// so each row compares the faster middle sample and never a slower one against
136 /// the floor. `values` must hold 1 to 64 samples, and the sort runs on a stack
137 /// copy. The scan benchmark keeps its own copy of this function.
138 fn lowerMedian(values: []const u64) u64 {
139 std.debug.assert(values.len > 0);
140 std.debug.assert(values.len <= maximum_samples);
141 var sorted: [maximum_samples]u64 = undefined;
142 @memcpy(sorted[0..values.len], values);
143 std.mem.sort(u64, sorted[0..values.len], {}, std.sort.asc(u64));
144 return sorted[(values.len - 1) / 2];
145 }
146
147 const Intervals = struct {
148 cold: Interval,
149 warm: Interval,
150 retained: Interval,
151 identity: Interval,
152 copy: Interval,
153 };
154
155 const Samples = struct {
156 cold: Series = .{},
157 warm: Series = .{},
158 retained: Series = .{},
159 identity: Series = .{},
160 copy: Series = .{},
161
162 fn record(self: *Samples, intervals: Intervals) void {
163 self.cold.record(intervals.cold);
164 self.warm.record(intervals.warm);
165 self.retained.record(intervals.retained);
166 self.identity.record(intervals.identity);
167 self.copy.record(intervals.copy);
168 }
169 };
170
171 /// The records embedded by one target stage record, counted by walking its
172 /// chain of predecessors for the benchmark to print as facts about the final
173 /// record. `bytes` is the total a compile writes to describe that chain, so a
174 /// change to the record's shape moves it.
175 const Closure = struct {
176 records: u32 = 0,
177 bytes: u64 = 0,
178 };
179
180 const Observation = struct {
181 intervals: Intervals,
182 cold: [stage_count]floor_mod.Stage,
183 warm: [stage_count]floor_mod.Stage,
184 retained: [stage_count]floor_mod.Stage,
185 target: floor_mod.Sealed,
186 closure: Closure,
187 };
188
189 const Preparation = struct {
190 prepared: *Prepared,
191 report: publication.PreparationReport,
192
193 fn deinit(self: *Preparation) void {
194 self.prepared.deinit();
195 self.report.deinit();
196 }
197
198 fn revisionAt(self: *const Preparation, index: usize) *const revision.Revision {
199 std.debug.assert(index < stage_count);
200 std.debug.assert(self.report.completed == stage_count);
201 return self.report.stages[index].record();
202 }
203 };
204
205 const Summary = struct {
206 family: []const u8 = "memory",
207 name: []const u8,
208 metric: []const u8,
209 unit: []const u8,
210 aggregation: []const u8 = "deterministic",
211 count: u32 = 1,
212 value: u64,
213 };
214
215 const RowInput = struct {
216 scope: []const u8,
217 series: *const Series,
218 derivation: floor.Derivation,
219 /// The cause measured for a wide gap on this row, or null when no run has
220 /// named one, carried so a wide gate can print it. No row sets it today, so
221 /// a publication gate never names a cause.
222 gap: ?floor.Gap = null,
223 };
224
225 /// Runs the warmup observations, then the measured samples, and prints the
226 /// record facts followed by one gate row per timed operation. The command calls
227 /// this to run the whole benchmark. Every sample must produce the same record
228 /// lengths as the first, else the run fails with
229 /// `error.NondeterministicRecords`. After printing every row, the run fails
230 /// with `error.FloorAboveMeasurement` when any floor exceeded its measurement.
231 pub fn run(
232 allocator: std.mem.Allocator,
233 out: *std.Io.Writer,
234 options: options_mod.Options,
235 ) !void {
236 std.debug.assert(options.samples > 0);
237 std.debug.assert(options.samples <= maximum_samples);
238 _ = try choir.product.compiler.manifest();
239 const destination = try allocator.alloc(u8, record_bytes);
240 defer allocator.free(destination);
241 @memset(destination, 0);
242 for (0..options.warmup) |_| _ = try observe(allocator, destination);
243 var samples = Samples{};
244 const first = try observe(allocator, destination);
245 samples.record(first.intervals);
246 coz.progressNamed("accy.publication.sample");
247 for (1..options.samples) |_| {
248 const next = try observe(allocator, destination);
249 try requireSameShape(&first, &next);
250 samples.record(next.intervals);
251 coz.progressNamed("accy.publication.sample");
252 }
253 try writeFacts(out, &first);
254 try writeRows(out, &samples, &first);
255 }
256
257 fn observe(allocator: std.mem.Allocator, destination: []u8) !Observation {
258 var observation: Observation = undefined;
259 const intervals = &observation.intervals;
260 var cold = try prepareDraft(allocator, null, &intervals.cold, "cold");
261 defer cold.deinit();
262 var warm = try prepareDraft(allocator, &cold, &intervals.warm, "warm");
263 defer warm.deinit();
264 const source = cold.report.source orelse return error.MissingSemanticSource;
265 const input = publication.SemanticInput{ .retained = source };
266 var retained = try prepareTimed(allocator, input, &cold, &intervals.retained, "retained");
267 defer retained.deinit();
268 var reference = try prepareDraft(allocator, null, null, "reference");
269 defer reference.deinit();
270 try describe(.cold, &cold, null, &observation.cold);
271 try describe(.warm, &warm, &cold, &observation.warm);
272 try describe(.retained, &retained, &cold, &observation.retained);
273 const target = cold.revisionAt(stage_count - 1);
274 intervals.identity = try compareRecords(target, reference.revisionAt(stage_count - 1));
275 intervals.copy = try copyRecord(target, destination);
276 observation.target = observation.cold[stage_count - 1].sealed;
277 observation.closure = try closureOf(target.view().semantic_record);
278 return observation;
279 }
280
281 fn prepareDraft(
282 allocator: std.mem.Allocator,
283 candidate: ?*const Preparation,
284 interval: ?*Interval,
285 comptime label: []const u8,
286 ) !Preparation {
287 const module = try buildChain(allocator);
288 defer module.deinit();
289 return prepareTimed(allocator, .{ .draft = module }, candidate, interval, label);
290 }
291
292 fn prepareTimed(
293 allocator: std.mem.Allocator,
294 input: publication.SemanticInput,
295 candidate: ?*const Preparation,
296 interval: ?*Interval,
297 comptime label: []const u8,
298 ) !Preparation {
299 var report = publication.PreparationReport{};
300 errdefer report.deinit();
301 const current = request(if (candidate) |item| item.prepared else null);
302 const scope = coz.scope("accy.publication." ++ label);
303 var timer = Timer.begin();
304 const result = publication.prepare(allocator, input, current, &report, configuration);
305 const measured = timer.end();
306 scope.end();
307 const prepared = try result;
308 if (interval) |output| output.* = measured;
309 return .{ .prepared = prepared, .report = report };
310 }
311
312 fn compareRecords(left: *const revision.Revision, right: *const revision.Revision) !Interval {
313 const left_inputs = left.view().exact.inputs;
314 const right_inputs = right.view().exact.inputs;
315 if (left_inputs.ptr == right_inputs.ptr) return error.SharedTargetRecord;
316 const scope = coz.scope("accy.publication.target.identity");
317 var timer = Timer.begin();
318 const equal = left.eql(right);
319 const measured = timer.end();
320 scope.end();
321 if (!equal) return error.TargetRecordsDiffer;
322 return measured;
323 }
324
325 fn copyRecord(target: *const revision.Revision, destination: []u8) !Interval {
326 const view = target.view();
327 if (view.semantic_record.len > destination.len) return error.RecordLimit;
328 const scope = coz.scope("accy.publication.target.copy");
329 var timer = Timer.begin();
330 const result = revision.record.encodeExactInto(destination, view.exact);
331 const measured = timer.end();
332 scope.end();
333 const written = try result;
334 if (!std.mem.eql(u8, written, view.semantic_record)) return error.RecordCopyDiffers;
335 return measured;
336 }
337
338 fn describe(
339 kind: floor_mod.Route,
340 current: *const Preparation,
341 candidate: ?*const Preparation,
342 output: *[stage_count]floor_mod.Stage,
343 ) !void {
344 for (output, 0..) |*stage, index| {
345 const execution = ¤t.report.stages[index];
346 const shared = sharesDependency(current, candidate, index);
347 stage.* = .{
348 .sealed = try sealedView(execution.record()),
349 .execution = switch (execution.*) {
350 .cold => .cold,
351 .warm => if (shared) .reused_shared else .reused,
352 },
353 };
354 }
355 if (!floor_mod.matches(kind, output)) return error.UnexpectedStageExecution;
356 }
357
358 /// Returns true when the kept predecessor record of stage `index` and the
359 /// candidate's stored record are the same bytes at the same address, the test
360 /// `Draft.eqlInputs` makes, so the benchmark tells whether a reused stage's
361 /// predecessor was shared or compared in full. The first stage, or a stage with
362 /// no candidate, never shares.
363 fn sharesDependency(
364 current: *const Preparation,
365 candidate: ?*const Preparation,
366 index: usize,
367 ) bool {
368 if (index == 0) return false;
369 const prior = candidate orelse return false;
370 const retained = current.revisionAt(index - 1).view().semantic_record;
371 const stored = prior.revisionAt(index - 1).view().semantic_record;
372 return retained.ptr == stored.ptr;
373 }
374
375 fn sealedView(item: *const revision.Revision) !floor_mod.Sealed {
376 const view = item.view();
377 const inputs = view.exact.inputs;
378 const decoded = item.inputs();
379 var dependency: []const u8 = &.{};
380 var dependencies = decoded.dependencies.iterator();
381 if (try dependencies.next()) |first| dependency = first.exact;
382 if (try dependencies.next() != null) return error.UnexpectedDependencies;
383 var offset: usize = 0;
384 if (dependency.len > 0) {
385 const start = @intFromPtr(dependency.ptr);
386 if (start < @intFromPtr(inputs.ptr)) return error.UnexpectedDependencies;
387 offset = start - @intFromPtr(inputs.ptr);
388 }
389 if (offset + dependency.len > inputs.len) return error.UnexpectedDependencies;
390 if (dependency.len > 0 and offset == 0) return error.UnexpectedDependencies;
391 if (view.exact.address.len == 0) return error.EmptyRecordRegion;
392 if (inputs.len == 0) return error.EmptyRecordRegion;
393 if (view.exact.image.len == 0) return error.EmptyRecordRegion;
394 if (view.compiler_manifest.len == 0) return error.EmptyRecordRegion;
395 if (view.semantic_record.len >= view.compiler_manifest.len) {
396 return error.UnexpectedRecordShape;
397 }
398 return .{
399 .address = @intCast(view.exact.address.len),
400 .inputs = @intCast(inputs.len),
401 .image = @intCast(view.exact.image.len),
402 .record = @intCast(view.semantic_record.len),
403 .manifest = @intCast(view.compiler_manifest.len),
404 .dependency = @intCast(dependency.len),
405 .dependency_offset = @intCast(offset),
406 };
407 }
408
409 fn closureOf(bytes: []const u8) !Closure {
410 var result = Closure{};
411 var current = bytes;
412 for (0..stage_count) |_| {
413 const exact = try revision.record.decodeExact(current);
414 const inputs = try revision.record.decodeInputs(exact.inputs);
415 result.records += 1;
416 result.bytes += current.len;
417 var dependencies = inputs.dependencies.iterator();
418 const dependency = try dependencies.next() orelse return result;
419 if (try dependencies.next() != null) return error.UnexpectedDependencies;
420 current = dependency.exact;
421 }
422 return error.UnexpectedDependencies;
423 }
424
425 fn requireSameShape(first: *const Observation, next: *const Observation) !void {
426 if (!std.meta.eql(first.cold, next.cold)) return error.NondeterministicRecords;
427 if (!std.meta.eql(first.warm, next.warm)) return error.NondeterministicRecords;
428 if (!std.meta.eql(first.retained, next.retained)) return error.NondeterministicRecords;
429 if (!std.meta.eql(first.closure, next.closure)) return error.NondeterministicRecords;
430 }
431
432 fn writeFacts(out: *std.Io.Writer, observation: *const Observation) !void {
433 const target = observation.target;
434 try writeSummary(out, .{
435 .name = "accy.publication",
436 .metric = "compiler_manifest_bytes",
437 .unit = "bytes",
438 .value = target.manifest,
439 });
440 for (observation.cold, 0..) |stage, index| {
441 const tag = @tagName(@as(PublicationStage, @fromBackingInt(@intCast(index))));
442 var name: [64]u8 = undefined;
443 const stage_scope = try std.fmt.bufPrint(&name, "accy.publication.{s}", .{tag});
444 try writeSummary(out, .{
445 .name = stage_scope,
446 .metric = "record_bytes",
447 .unit = "bytes",
448 .value = stage.sealed.record,
449 });
450 }
451 try writeClosure(out, observation);
452 try writeRequired(out, "accy.publication.cold", &observation.cold);
453 try writeRequired(out, "accy.publication.warm", &observation.warm);
454 try writeRequired(out, "accy.publication.retained", &observation.retained);
455 }
456
457 fn writeClosure(out: *std.Io.Writer, observation: *const Observation) !void {
458 const embedded = observation.closure;
459 const scope = "accy.publication.target";
460 std.debug.assert(embedded.records <= stage_count);
461 std.debug.assert(observation.target.record <= record_bytes);
462 const facts = [_]Summary{
463 .{ .name = scope, .metric = "closure_records", .unit = "count", .value = embedded.records },
464 .{
465 .name = scope,
466 .metric = "closure_bytes",
467 .unit = "bytes",
468 .value = embedded.bytes,
469 },
470 .{
471 .name = scope,
472 .metric = "record_headroom_bytes",
473 .unit = "bytes",
474 .value = record_bytes - observation.target.record,
475 },
476 };
477 for (facts) |fact| try writeSummary(out, fact);
478 }
479
480 fn writeRequired(
481 out: *std.Io.Writer,
482 scope: []const u8,
483 stages: *const [stage_count]floor_mod.Stage,
484 ) !void {
485 const totals = floor_mod.required(stages);
486 try writeSummary(out, .{
487 .name = scope,
488 .metric = "required_copy_bytes",
489 .unit = "bytes",
490 .value = totals.copy_bytes,
491 });
492 try writeSummary(out, .{
493 .name = scope,
494 .metric = "required_compare_bytes",
495 .unit = "bytes",
496 .value = totals.compare_bytes,
497 });
498 }
499
500 fn writeSummary(out: *std.Io.Writer, summary: Summary) !void {
501 std.debug.assert(summary.name.len > 0);
502 std.debug.assert(summary.count > 0);
503 try out.print(
504 "{{\"schema\":\"{s}\",\"event\":\"summary\",\"family\":\"{s}\"," ++
505 "\"metric\":\"{s}\",\"unit\":\"{s}\",\"aggregation\":\"{s}\"," ++
506 "\"name\":\"{s}\",\"count\":{d},\"value\":{d}}}\n",
507 .{
508 floor.metric_schema, summary.family, summary.metric, summary.unit,
509 summary.aggregation, summary.name, summary.count, summary.value,
510 },
511 );
512 }
513
514 fn writeRows(
515 out: *std.Io.Writer,
516 samples: *const Samples,
517 observation: *const Observation,
518 ) !void {
519 var entries: [5]floor_mod.Entries = @splat(.{});
520 const rows = [_]RowInput{ .{
521 .scope = "accy.publication.cold",
522 .series = &samples.cold,
523 .derivation = floor_mod.route(.cold, &observation.cold, &entries[0]),
524 }, .{
525 .scope = "accy.publication.warm",
526 .series = &samples.warm,
527 .derivation = floor_mod.route(.warm, &observation.warm, &entries[1]),
528 }, .{
529 .scope = "accy.publication.retained",
530 .series = &samples.retained,
531 .derivation = floor_mod.route(.retained, &observation.retained, &entries[2]),
532 }, .{
533 .scope = "accy.publication.target.identity",
534 .series = &samples.identity,
535 .derivation = floor_mod.identity(observation.target, &entries[3]),
536 }, .{
537 .scope = "accy.publication.target.copy",
538 .series = &samples.copy,
539 .derivation = floor_mod.copy(observation.target, &entries[4]),
540 } };
541 var defects: u8 = 0;
542 for (rows) |row| defects += @intFromBool(try writeRow(out, row));
543 if (defects == 0) return;
544 try out.flush();
545 return error.FloorAboveMeasurement;
546 }
547
548 /// Prints one gate with its coverage, then a clock metric when every sample
549 /// counted its cycles, then median page faults and task clock when the software
550 /// counters worked. The benchmark calls this once per timed row. The function
551 /// returns true, after printing a `profile defect` line, when the floor exceeds
552 /// the measurement. The gate carries the row's gap only when the ratio
553 /// exceeds 10.
554 fn writeRow(out: *std.Io.Writer, row: RowInput) !bool {
555 const series = row.series;
556 const estimate = try row.derivation.estimate(null);
557 const floor_ns = try estimate.totalNs();
558 const measured_ns = lowerMedian(series.ns[0..series.len]);
559 std.debug.assert(floor_ns > 0);
560 std.debug.assert(measured_ns > 0);
561 const exceeded = floor.ceilingRatio(measured_ns, floor_ns) > gap_ratio;
562 const gate = floor.Gate{
563 .scope = row.scope,
564 .floor_ns = floor_ns,
565 .measured_ns = measured_ns,
566 .gap = if (exceeded) row.gap else null,
567 };
568 try out.print("{f}", .{gate});
569 try writeCoverage(out, row.scope, row.derivation.coverage);
570 if (series.clock()) |observed| {
571 const metric = floor.Row{
572 .scope = row.scope,
573 .estimate = estimate,
574 .measured_ns = measured_ns,
575 .clock = observed,
576 .gap = gate.gap,
577 };
578 try metric.writeMetric(out);
579 }
580 if (series.software_counted) try writeKernelShare(out, row.scope, series);
581 if (floor_ns <= measured_ns) return false;
582 try out.print("profile defect {s}: floor={d}ns measured={d}ns\n", .{
583 row.scope, floor_ns, measured_ns,
584 });
585 return true;
586 }
587
588 /// States whether the floor covers all of the row's work, and lists each
589 /// unpriced piece on its own line when unpriced pieces remain. `writeRow` calls
590 /// this function after each gate line. A wide ratio then reads as work left
591 /// unpriced, named in the lines below it. The scan benchmark keeps an identical
592 /// copy of this function.
593 fn writeCoverage(out: *std.Io.Writer, scope: []const u8, coverage: floor.Coverage) !void {
594 switch (coverage) {
595 .complete => try out.print("profile coverage {s}: complete\n", .{scope}),
596 .partial => |names| {
597 try out.print("profile coverage {s}: partial, unpriced={d}\n", .{ scope, names.len });
598 for (names) |name| try out.print("profile unpriced {s}: {s}\n", .{ scope, name });
599 },
600 }
601 }
602
603 fn writeKernelShare(out: *std.Io.Writer, scope: []const u8, series: *const Series) !void {
604 std.debug.assert(series.software_counted);
605 std.debug.assert(series.len > 0);
606 try writeSummary(out, .{
607 .name = scope,
608 .metric = "page_faults",
609 .unit = "count",
610 .aggregation = "median",
611 .count = series.len,
612 .value = lowerMedian(series.page_faults[0..series.len]),
613 });
614 try writeSummary(out, .{
615 .family = "timing",
616 .name = scope,
617 .metric = "task_clock_ns",
618 .unit = "ns",
619 .aggregation = "median",
620 .count = series.len,
621 .value = lowerMedian(series.task_ns[0..series.len]),
622 });
623 }
624
625 fn request(candidate: ?*const Prepared) publication.PreparationRequest {
626 return .{
627 .source = "accy-publication-bench",
628 .work = .{
629 .allowance = revision.WorkVector.uniform(std.math.maxInt(u64)),
630 .workspace = workspace_bytes,
631 .events = work_events,
632 },
633 .record_bytes = record_bytes,
634 .candidate = candidate,
635 };
636 }
637
638 fn buildChain(allocator: std.mem.Allocator) !*accy.choir.SemanticModule {
639 var builder = try accy.choir.SemanticBuilder.init(allocator, .standard);
640 defer builder.deinit();
641 const tensor = try builder.tensor(.f32, &.{chain_elements});
642 var function = try builder.beginFunction(
643 "accy_publication_chain",
644 &.{ tensor, tensor },
645 &.{tensor},
646 );
647 var current = function.parameter(0);
648 const operand = function.parameter(1);
649 for (0..chain_length) |index| {
650 current = switch (index % 4) {
651 0 => try function.add(current, operand),
652 1 => try function.mul(current, operand),
653 2 => try function.sub(current, operand),
654 else => try function.max(current, operand),
655 };
656 }
657 try function.return_(&.{current});
658 try function.finish();
659 return builder.finish();
660 }
661
662 fn registerContext(context: *choir.ir.Context) !void {
663 try choir.dialects.registerChoirDialect(context);
664 try accy.choir.registerAccyDialect(context);
665 try choir.backends.gpu.registerTargetDialects(context);
666 }