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 }