lib/accy/src/executable/composition/cpu/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gpu = @import("gpu");
3 const choir = @import("choir");
4 const sys = @import("sys");
5 const accy_root = @import("../../../root.zig");
6 const fixture = @import("../../../fixture/root.zig");
7 const executable = @import("../../root.zig");
8 const composition = @import("../root.zig");
9 const namespace = @import("root.zig");
10 const compilation = namespace.compilation;
11 const materialization = namespace.materialization;
12 const runtime = namespace.runtime;
13 const CpuObjectCompileOptions = namespace.CpuObjectCompileOptions;
14 const CpuObjectCompilation = namespace.CpuObjectCompilation;
15 const RuntimeEvidence = namespace.RuntimeEvidence;
16 const RuntimeEvidenceError = namespace.RuntimeEvidenceError;
17 const compileCpuObject = namespace.compileCpuObject;
18 const materializeCpuObject = namespace.materializeCpuObject;
19 const runtimeEvidence = namespace.runtimeEvidence;
20
21 const Allocator = std.mem.Allocator;
22 const accy = accy_root.choir;
23 const ChoirComposition = choir.composition;
24 const publication = fixture.publication;
25
26 fn addSemanticModule(allocator: Allocator, name: []const u8) !*accy.SemanticModule {
27 var builder = try accy.SemanticBuilder.init(allocator, accy.SemanticBuilder.ContextLimits.standard);
28 errdefer builder.deinit();
29 const f32_8 = try builder.tensor(.f32, &.{8});
30 var function = try builder.beginFunction(name, &.{ f32_8, f32_8 }, &.{f32_8});
31 const sum = try function.add(function.parameter(0), function.parameter(1));
32 try function.return_(&.{sum});
33 try function.finish();
34 return try builder.finish();
35 }
36
37 const InvocationWorker = struct {
38 fragment: ChoirComposition.LoadedFragment,
39 call_site: *const ChoirComposition.CallSite,
40 context: *ChoirComposition.abi.Context,
41 frame: *ChoirComposition.abi.Frame,
42 status: ChoirComposition.abi.Status = .fragment_failure,
43
44 fn run(self: *InvocationWorker) void {
45 self.status = self.fragment.vtable.invoke.?(self.fragment.state, self.call_site, self.context, self.frame);
46 }
47 };
48
49 test "Accy composition CPU object compilation consumes semantic module on first allocation failure" {
50 const module = try addSemanticModule(std.testing.allocator, "composition_cpu_object_oom");
51 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
52 failing.fail_index = failing.alloc_index;
53 var report = accy_root.preparation.publication.PreparationReport{};
54 defer report.deinit();
55 try std.testing.expectError(error.OutOfMemory, compileCpuObject(
56 failing.allocator(),
57 .{ .value = 23 },
58 module,
59 .{
60 .request = publication.request("composition_cpu_object_oom", &.{}),
61 .compiler = .{ .launch_tuning_artifact = &.{1} },
62 },
63 &report,
64 publication.configuration,
65 ));
66 }
67
68 test "Accy composition CPU object exhaustion consumes its draft without native success" {
69 const module = try addSemanticModule(std.testing.allocator, "composition_cpu_exhausted");
70 var report = accy_root.preparation.publication.PreparationReport{};
71 defer report.deinit();
72 var request = publication.request("composition_cpu_exhausted", &.{});
73 request.work.allowance.structural_visits = 0;
74 try std.testing.expectError(error.WorkExhausted, compileCpuObject(
75 std.testing.allocator,
76 .{ .value = 24 },
77 module,
78 .{ .request = request },
79 &report,
80 publication.configuration,
81 ));
82 try std.testing.expectEqual(@as(u8, 0), report.completed);
83 const failure = report.failure orelse return error.TestExpectedResult;
84 try std.testing.expectEqual(.exhausted, failure.work.outcome);
85 }
86
87 test "Accy composition CPU object metadata survives every allocation failure" {
88 const allocator = std.testing.allocator;
89 const semantic_module = try addSemanticModule(allocator, "composition_cpu_metadata_oom");
90 var backend_state = gpu.cpu.State.init(allocator);
91 defer backend_state.deinit();
92 const compiled_fragment = executable.compileFragmentFromSemanticModule(
93 allocator,
94 backend_state.handle(),
95 semantic_module,
96 .{ .artifact_format = .cpu_object },
97 ) catch |err| switch (err) {
98 error.UnsupportedOperation => return error.SkipZigTest,
99 else => return err,
100 };
101 defer compiled_fragment.deinit();
102
103 const Harness = struct {
104 fn run(
105 failing_allocator: Allocator,
106 fragment: *const executable.CompiledFragment,
107 ) !void {
108 var metadata = try composition.CpuObjectMetadata.init(failing_allocator, fragment);
109 defer metadata.deinit();
110 }
111 };
112 try fixture.checkAllAllocationFailures(Harness.run, .{compiled_fragment});
113 }
114
115 test "Accy composition CPU object retains artifacts and isolates concurrent invocations" {
116 const allocator = std.testing.allocator;
117 const semantic_module = try addSemanticModule(allocator, "composition_cpu_object_add");
118 const launch_tuning_artifact = try executable.encodeLaunchTuningArtifact(allocator, &.{});
119 defer allocator.free(launch_tuning_artifact);
120 const workspace = try allocator.alloc(u8, publication.artifact_workspace_bytes);
121 defer allocator.free(workspace);
122 var report = accy_root.preparation.publication.PreparationReport{};
123 defer report.deinit();
124 var compilation_value = compileCpuObject(allocator, .{ .value = 23 }, semantic_module, .{
125 .request = publication.request("composition_cpu_object_add", workspace),
126 .compiler = .{ .launch_tuning_artifact = launch_tuning_artifact },
127 }, &report, publication.configuration) catch |err| switch (err) {
128 error.UnsupportedOperation => return error.SkipZigTest,
129 else => return err,
130 };
131 defer compilation_value.deinit();
132 try std.testing.expectEqual(@as(u8, 7), report.completed);
133 try std.testing.expect(compilation_value.input_product.record.eql(
134 report.stages[0].record().metadata(),
135 ));
136 report.deinit();
137 @memset(workspace, 0xa5);
138 try compilation_value.input_product.validate();
139 try std.testing.expect(compilation_value.launch_tuning_artifact.ptr != launch_tuning_artifact.ptr);
140
141 try std.testing.expectEqual(@as(usize, 1), compilation_value.metadata.artifacts.len);
142 try std.testing.expectEqual(@as(usize, 1), choir.backends.artifact.resource.count(compilation_value.metadata.artifacts[0]));
143 try std.testing.expectEqual(choir.backends.artifact.ArtifactKind.object_file, compilation_value.metadata.artifacts[0].metadata.kind);
144 try std.testing.expectEqual(@as(usize, 2), compilation_value.metadata.input_boundaries.len);
145 try std.testing.expectEqual(@as(usize, 1), compilation_value.metadata.output_boundaries.len);
146 for (compilation_value.metadata.input_boundaries) |boundary| {
147 try std.testing.expectEqual(ChoirComposition.ElementType.f32, boundary.element_type);
148 try std.testing.expectEqualSlices(u64, &.{8}, boundary.dimensions);
149 try std.testing.expectEqual(@as(usize, 32), boundary.byte_size);
150 }
151 try std.testing.expectEqual(ChoirComposition.ElementType.f32, compilation_value.metadata.output_boundaries[0].element_type);
152 try std.testing.expectEqualSlices(u64, &.{8}, compilation_value.metadata.output_boundaries[0].dimensions);
153 try std.testing.expectEqual(@as(usize, 32), compilation_value.metadata.output_boundaries[0].byte_size);
154 const entry_symbol = compilation_value.metadata.entrySymbol() orelse return error.TestExpectedResult;
155 try std.testing.expect(entry_symbol.len != 0);
156 try std.testing.expect(compilation_value.metadata.artifacts[0].linkage.hasProvided(entry_symbol));
157
158 var source_module = try ChoirComposition.PartitionedModule.init(allocator, "composition_cpu_object");
159 defer source_module.deinit();
160 try source_module.addBoundary(.{ .id = .{ .value = 1 }, .name = "lhs", .element_type = .f32, .dimensions = &.{8}, .byte_size = 32, .access = .read, .ownership = .borrowed, .alias = .disjoint, .provenance = .{ .path = "composition", .symbol = "lhs" } });
161 try source_module.addBoundary(.{ .id = .{ .value = 2 }, .name = "rhs", .element_type = .f32, .dimensions = &.{8}, .byte_size = 32, .access = .read, .ownership = .borrowed, .alias = .disjoint, .provenance = .{ .path = "composition", .symbol = "rhs" } });
162 try source_module.addBoundary(.{ .id = .{ .value = 3 }, .name = "sum", .element_type = .f32, .dimensions = &.{8}, .byte_size = 32, .access = .write, .ownership = .produced, .alias = .disjoint, .provenance = .{ .path = "composition", .symbol = "sum" } });
163 try source_module.addPartition(.{
164 .id = .{ .value = 13 },
165 .name = "add",
166 .pipeline = .accy,
167 .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
168 .outputs = &.{.{ .value = 3 }},
169 .product = compilation_value.input_product.ref,
170 .effect = .none,
171 .provenance = .{ .path = "composition", .symbol = "add" },
172 });
173 var durable_module = try ChoirComposition.CompositionModule.init(allocator, &source_module);
174 defer durable_module.deinit();
175 try durable_module.addFragment(.{
176 .id = compilation_value.fragment_id,
177 .partition = .{ .value = 13 },
178 .pipeline = .accy,
179 .pipeline_input = compilation_value.input_product,
180 .artifacts = compilation_value.metadata.artifacts,
181 .exports = &.{.{ .name = "add", .symbol = entry_symbol, .abi_version = ChoirComposition.abi.version }},
182 .provenance = .{ .path = "composition", .symbol = "add" },
183 });
184 try durable_module.addVariant(.{
185 .id = .{ .value = 1 },
186 .choice = .{
187 .target = choir.product.productRef("composition", "cpu", "target", "x86_64"),
188 .policy = choir.product.productRef("composition", "cpu", "selection", "native"),
189 },
190 .fragments = &.{compilation_value.fragment_id},
191 .call_sites = &.{},
192 });
193 const durable_fragment = durable_module.fragment(compilation_value.fragment_id) orelse return error.TestExpectedResult;
194 const durable_lhs = &durable_module.source_module.boundaries.items[0];
195 const original_element_type = durable_lhs.element_type;
196 durable_lhs.element_type = .i32;
197 const type_mismatch = materializeCpuObject(&compilation_value, allocator, &durable_module, durable_fragment);
198 durable_lhs.element_type = original_element_type;
199 try std.testing.expectError(error.BoundaryMismatch, type_mismatch);
200
201 const original_dimensions = durable_lhs.dimensions;
202 const mismatched_dimensions = [_]u64{ 4, 2 };
203 durable_lhs.dimensions = &mismatched_dimensions;
204 const shape_mismatch = materializeCpuObject(&compilation_value, allocator, &durable_module, durable_fragment);
205 durable_lhs.dimensions = original_dimensions;
206 try std.testing.expectError(error.BoundaryMismatch, shape_mismatch);
207
208 const materialization_allocation_count = 3;
209 for (0..materialization_allocation_count) |fail_index| {
210 var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = fail_index });
211 try std.testing.expectError(
212 error.OutOfMemory,
213 materializeCpuObject(&compilation_value, failing.allocator(), &durable_module, durable_fragment),
214 );
215 try std.testing.expect(compilation_value.compiled_fragment != null);
216 }
217
218 const materialized = try materializeCpuObject(&compilation_value, allocator, &durable_module, durable_fragment);
219 try std.testing.expect(compilation_value.compiled_fragment == null);
220 const loaded_fragment = ChoirComposition.LoadedFragment{
221 .id = compilation_value.fragment_id,
222 .state = materialized.state,
223 .vtable = materialized.vtable,
224 };
225 defer loaded_fragment.vtable.deinit(loaded_fragment.state, allocator);
226
227 var provenance = try ChoirComposition.Provenance.init(allocator, .{ .path = "composition", .symbol = "add" });
228 defer provenance.deinit(allocator);
229 const call_site = ChoirComposition.CallSite{
230 .id = .{ .value = 41 },
231 .name = "add",
232 .caller = .{ .value = 11 },
233 .caller_export = "host",
234 .runtime_import = ChoirComposition.abi.invoke_symbol,
235 .callee = loaded_fragment.id,
236 .callee_export = "composition_cpu_object_add",
237 .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
238 .outputs = &.{.{ .value = 3 }},
239 .abi_version = ChoirComposition.abi.version,
240 .provenance = provenance,
241 };
242
243 var context = ChoirComposition.abi.Context.init(0x1000, 0x2000);
244 var first_lhs = [_]f32{ 1, 2, 3, 4, 5, 6, 7, 8 };
245 var first_rhs = [_]f32{ 8, 7, 6, 5, 4, 3, 2, 1 };
246 var first_output = @as([8]f32, @splat(0));
247 var first_resources = [_]ChoirComposition.abi.Resource{
248 ChoirComposition.abi.Resource.init(context.owner_cookie, 1, std.mem.sliceAsBytes(first_lhs[0..]), .read),
249 ChoirComposition.abi.Resource.init(context.owner_cookie, 1, std.mem.sliceAsBytes(first_rhs[0..]), .read),
250 ChoirComposition.abi.Resource.init(context.owner_cookie, 1, std.mem.sliceAsBytes(first_output[0..]), .write),
251 };
252 var first_frame = ChoirComposition.abi.Frame.init(context.owner_cookie, 1, &first_resources);
253 first_resources[2].access = @backingInt(ChoirComposition.Access.read);
254 try std.testing.expectEqual(
255 ChoirComposition.abi.Status.invalid_resource,
256 loaded_fragment.vtable.invoke.?(
257 loaded_fragment.state,
258 &call_site,
259 &context,
260 &first_frame,
261 ),
262 );
263 first_resources[2].access = @backingInt(ChoirComposition.Access.write);
264
265 var second_lhs = [_]f32{ -1, -2, -3, -4, 9, 10, 11, 12 };
266 var second_rhs = [_]f32{ 2, 4, 6, 8, -1, -2, -3, -4 };
267 var second_output = @as([8]f32, @splat(0));
268 var second_resources = [_]ChoirComposition.abi.Resource{
269 ChoirComposition.abi.Resource.init(context.owner_cookie, 2, std.mem.sliceAsBytes(second_lhs[0..]), .read),
270 ChoirComposition.abi.Resource.init(context.owner_cookie, 2, std.mem.sliceAsBytes(second_rhs[0..]), .read),
271 ChoirComposition.abi.Resource.init(context.owner_cookie, 2, std.mem.sliceAsBytes(second_output[0..]), .write),
272 };
273 var second_frame = ChoirComposition.abi.Frame.init(context.owner_cookie, 2, &second_resources);
274
275 var first_worker = InvocationWorker{
276 .fragment = loaded_fragment,
277 .call_site = &call_site,
278 .context = &context,
279 .frame = &first_frame,
280 };
281 var second_worker = InvocationWorker{
282 .fragment = loaded_fragment,
283 .call_site = &call_site,
284 .context = &context,
285 .frame = &second_frame,
286 };
287 const first_thread = try sys.thread.spawn(InvocationWorker.run, .{&first_worker});
288 const second_thread = sys.thread.spawn(InvocationWorker.run, .{&second_worker}) catch |err| {
289 first_thread.join();
290 return err;
291 };
292 first_thread.join();
293 second_thread.join();
294
295 try std.testing.expectEqual(ChoirComposition.abi.Status.ok, first_worker.status);
296 try std.testing.expectEqual(ChoirComposition.abi.Status.ok, second_worker.status);
297 for (first_output, 0..) |value, index| try std.testing.expectEqual(first_lhs[index] + first_rhs[index], value);
298 for (second_output, 0..) |value, index| try std.testing.expectEqual(second_lhs[index] + second_rhs[index], value);
299
300 const evidence = try runtimeEvidence(loaded_fragment);
301 try std.testing.expectEqual(@as(u64, 2), evidence.invocation_count);
302 try std.testing.expectEqual(@as(u64, 2), evidence.completed_invocation_count);
303 try std.testing.expectEqual(@as(u64, 0), evidence.failed_invocation_count);
304 try std.testing.expectEqual(@as(usize, 1), evidence.loaded_artifact_count);
305 try std.testing.expectEqual(@as(usize, 0), evidence.live_buffer_count);
306 }
307
308 test {
309 @import("test_discovery").discover(namespace);
310 _ = compilation;
311 _ = materialization;
312 _ = runtime;
313 }
314
315 test "accy executable composition cpu declaration coverage" {
316 std.testing.refAllDecls(namespace);
317 }