lib/accy/src/executable/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const namespace = @import("root.zig");
2 const std = @import("std");
3 const gpu = @import("gpu");
4 const choir = @import("choir");
5 const accy_root = @import("../root.zig");
6 const exec_product = @import("plan.zig");
7 const tuning_mod = @import("tuning.zig");
8 const binding_mod = @import("binding.zig");
9 const loaded_mod = @import("loaded.zig");
10 const fixture = @import("fixture.zig");
11 const passes = choir.passes;
12 const LaunchCandidateMeasurement = tuning_mod.LaunchCandidateMeasurement;
13 const LaunchTuningSelection = tuning_mod.LaunchTuningSelection;
14 const launch_tuning_cache_record_version = tuning_mod.launch_tuning_cache_record_version;
15 const LaunchTuningCacheRecord = tuning_mod.LaunchTuningCacheRecord;
16 const encodeLaunchTuningArtifact = tuning_mod.encodeLaunchTuningArtifact;
17 const decodeLaunchTuningArtifact = tuning_mod.decodeLaunchTuningArtifact;
18 const LaunchTuningCacheKey = tuning_mod.LaunchTuningCacheKey;
19 const LaunchTuningCache = tuning_mod.LaunchTuningCache;
20 const LaunchGraphNode = exec_product.LaunchGraphNode;
21 const LaunchGraphDependency = exec_product.LaunchGraphDependency;
22 const LaunchGraphLoopCarry = exec_product.LaunchGraphLoopCarry;
23 const LaunchGraphLoop = exec_product.LaunchGraphLoop;
24 const createDataflowLaunchGraphPlan = exec_product.createDataflowLaunchGraphPlan;
25 const loadKernels = loaded_mod.loadKernels;
26 const RecordingBackendState = gpu.recording.BackendState;
27 const addChoirModule = fixture.addChoirModule;
28 const escapedTwoKernelChoirModule = fixture.escapedTwoKernelChoirModule;
29 const createTestBackendArtifactPlan = fixture.createTestBackendArtifactPlan;
30 const slotBindingsForKernel = fixture.slotBindingsForKernel;
31 const slotBindingsForPlan = fixture.slotBindingsForPlan;
32 const elementCountBindingsForPlan = fixture.elementCountBindingsForPlan;
33 const firstElementCountBinding = fixture.firstElementCountBinding;
34 const testing = std.testing;
35
36 test {
37 _ = @import("composition/test.zig");
38 @import("test_discovery").discover(namespace);
39 @import("test_discovery").discover(namespace.fragment);
40 @import("test_discovery").discover(namespace.composition);
41 @import("test_discovery").discover(namespace.compiler);
42 @import("test_discovery").discover(namespace.invocation);
43 @import("test_discovery").discover(namespace.schedule);
44 }
45
46 test "accy executable declaration coverage" {
47 std.testing.refAllDecls(namespace);
48 std.testing.refAllDecls(namespace.fragment);
49 std.testing.refAllDecls(namespace.composition);
50 std.testing.refAllDecls(namespace.compiler);
51 std.testing.refAllDecls(namespace.invocation);
52 std.testing.refAllDecls(namespace.schedule);
53 }
54
55 test "Choir executable launch graph forwards per-kernel stream event dependencies" {
56 const allocator = testing.allocator;
57
58 var owned = try escapedTwoKernelChoirModule(allocator, "choir_cuda_executable_launch_graph");
59 defer owned.deinit();
60
61 var cache = passes.AnalysisCache.init(allocator, null);
62 defer cache.deinit();
63 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
64 defer pass_ctx.deinit();
65
66 var state = RecordingBackendState{
67 .allocator = allocator,
68 .kind = .cuda,
69 .format = .cuda_ptx,
70 };
71 const handle = state.handle();
72
73 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
74 defer artifact_plan.deinit();
75 try testing.expectEqual(@as(usize, 2), artifact_plan.kernelCount());
76
77 var executable = try loadKernels(allocator, handle, &artifact_plan);
78 defer executable.deinit();
79
80 const slot_bindings = try slotBindingsForPlan(allocator, &artifact_plan, .cuda);
81 defer allocator.free(slot_bindings);
82 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
83 defer allocator.free(count_bindings);
84
85 const stream_a = gpu.StreamHandle{ .id = 71, .backend = .cuda };
86 const stream_b = gpu.StreamHandle{ .id = 72, .backend = .cuda };
87 const dependency = gpu.EventHandle{ .id = 91, .backend = .cuda };
88 const done = gpu.EventHandle{ .id = 92, .backend = .cuda };
89 const second_waits = [_]gpu.EventHandle{dependency};
90 const nodes = [_]LaunchGraphNode{
91 .{
92 .kernel_index = 0,
93 .stream = stream_a,
94 .signal_event = dependency,
95 },
96 .{
97 .kernel_index = 1,
98 .stream = stream_b,
99 .wait_events = &second_waits,
100 .signal_event = done,
101 },
102 };
103 const dependencies = [_]LaunchGraphDependency{.{
104 .producer_node_index = 0,
105 .consumer_node_index = 1,
106 .slot_id = artifact_plan.kernels.items[0].output_slot_id,
107 }};
108
109 try executable.launchGraph(
110 allocator,
111 &artifact_plan,
112 slot_bindings,
113 count_bindings,
114 .{
115 .nodes = &nodes,
116 .dependencies = &dependencies,
117 },
118 );
119
120 const lowered_dependency = state.created_events[0];
121 try testing.expectEqual(@as(usize, 2), state.launch_count);
122 try testing.expectEqual(@as(usize, 1), state.created_event_count);
123 try testing.expectEqual(@as(gpu.BackendObjectId, 71), state.launch_streams[0].?);
124 try testing.expectEqual(@as(gpu.BackendObjectId, 91), state.launch_signal_events[0].?);
125 try testing.expectEqual(@as(gpu.BackendObjectId, 72), state.launch_streams[1].?);
126 try testing.expectEqual(@as(usize, 2), state.launch_wait_counts[1]);
127 try testing.expectEqual(@as(gpu.BackendObjectId, 91), state.launch_wait_events[1][0]);
128 try testing.expectEqual(lowered_dependency, state.launch_wait_events[1][1]);
129 try testing.expectEqual(@as(gpu.BackendObjectId, 92), state.launch_signal_events[1].?);
130 }
131
132 test "Choir executable launch graph host loops remap carry slots" {
133 const allocator = testing.allocator;
134
135 var owned = try escapedTwoKernelChoirModule(allocator, "choir_cuda_executable_launch_graph_host_loop");
136 defer owned.deinit();
137
138 var cache = passes.AnalysisCache.init(allocator, null);
139 defer cache.deinit();
140 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
141 defer pass_ctx.deinit();
142
143 var state = RecordingBackendState{
144 .allocator = allocator,
145 .kind = .cuda,
146 .format = .cuda_ptx,
147 };
148 const handle = state.handle();
149
150 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
151 defer artifact_plan.deinit();
152 try testing.expectEqual(@as(usize, 2), artifact_plan.kernelCount());
153
154 var executable = try loadKernels(allocator, handle, &artifact_plan);
155 defer executable.deinit();
156
157 const slot_bindings = try slotBindingsForPlan(allocator, &artifact_plan, .cuda);
158 defer allocator.free(slot_bindings);
159 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
160 defer allocator.free(count_bindings);
161
162 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
163 defer graph.deinit();
164
165 const initial_slot_id = artifact_plan.kernels.items[0].input_slot_ids[0];
166 const output_slot_id = artifact_plan.kernels.items[1].output_slot_id;
167 const carry = [_]LaunchGraphLoopCarry{.{
168 .initial_slot_id = initial_slot_id,
169 .input_slot_id = initial_slot_id,
170 .output_slot_id = output_slot_id,
171 .final_slot_id = output_slot_id,
172 }};
173 const loops = [_]LaunchGraphLoop{.{
174 .first_node_index = 0,
175 .node_count = graph.nodes.len,
176 .trip_count = 3,
177 .carries = &carry,
178 }};
179
180 try executable.launchGraph(
181 allocator,
182 &artifact_plan,
183 slot_bindings,
184 count_bindings,
185 .{
186 .nodes = graph.nodes,
187 .dependencies = graph.dependencies,
188 .loops = &loops,
189 },
190 );
191
192 const initial_binding = try binding_mod.bindingForSlot(slot_bindings, initial_slot_id, .read_only);
193 const output_binding = try binding_mod.bindingForSlot(slot_bindings, output_slot_id, .read_only);
194
195 try testing.expectEqual(@as(usize, 6), state.launch_count);
196 try testing.expect(state.launch_buffer_counts[0] >= 2);
197 try testing.expect(state.launch_buffer_counts[1] >= 1);
198 try testing.expectEqual(initial_binding.handle.id, state.launch_buffer_ids[0][1]);
199 try testing.expectEqual(output_binding.handle.id, state.launch_buffer_ids[1][0]);
200 try testing.expectEqual(output_binding.handle.id, state.launch_buffer_ids[2][1]);
201 try testing.expectEqual(initial_binding.handle.id, state.launch_buffer_ids[3][0]);
202 try testing.expectEqual(initial_binding.handle.id, state.launch_buffer_ids[4][1]);
203 try testing.expectEqual(output_binding.handle.id, state.launch_buffer_ids[5][0]);
204 }
205
206 test "Choir executable launch graph lowers dependencies to backend events" {
207 const allocator = testing.allocator;
208
209 var owned = try escapedTwoKernelChoirModule(allocator, "choir_cuda_executable_launch_graph_event_lowering");
210 defer owned.deinit();
211
212 var cache = passes.AnalysisCache.init(allocator, null);
213 defer cache.deinit();
214 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
215 defer pass_ctx.deinit();
216
217 var state = RecordingBackendState{
218 .allocator = allocator,
219 .kind = .cuda,
220 .format = .cuda_ptx,
221 };
222 const handle = state.handle();
223
224 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
225 defer artifact_plan.deinit();
226 try testing.expectEqual(@as(usize, 2), artifact_plan.kernelCount());
227
228 var executable = try loadKernels(allocator, handle, &artifact_plan);
229 defer executable.deinit();
230
231 const slot_bindings = try slotBindingsForPlan(allocator, &artifact_plan, .cuda);
232 defer allocator.free(slot_bindings);
233 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
234 defer allocator.free(count_bindings);
235
236 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
237 defer graph.deinit();
238 graph.nodes[0].stream = .{ .id = 71, .backend = .cuda };
239 graph.nodes[1].stream = .{ .id = 72, .backend = .cuda };
240
241 try executable.launchGraphWithDependencyEvents(
242 allocator,
243 &artifact_plan,
244 slot_bindings,
245 count_bindings,
246 graph.plan(),
247 );
248
249 try testing.expectEqual(@as(usize, 1), state.created_event_count);
250 const dependency_event = state.created_events[0];
251 try testing.expectEqual(@as(usize, 2), state.launch_count);
252 try testing.expectEqual(@as(gpu.BackendObjectId, 71), state.launch_streams[0].?);
253 try testing.expectEqual(@as(gpu.BackendObjectId, 72), state.launch_streams[1].?);
254 try testing.expectEqual(@as(usize, 1), state.launch_wait_counts[1]);
255 try testing.expectEqual(dependency_event, state.launch_wait_events[1][0]);
256 try testing.expectEqual(@as(usize, 1), state.record_event_count);
257 try testing.expectEqual(@as(gpu.BackendObjectId, 71), state.record_streams[0].?);
258 try testing.expectEqual(dependency_event, state.record_events[0].?);
259 }
260
261 test "Choir executable launch graph automatically lowers cross-stream dependencies" {
262 const allocator = testing.allocator;
263
264 var owned = try escapedTwoKernelChoirModule(allocator, "choir_cuda_executable_launch_graph_auto_events");
265 defer owned.deinit();
266
267 var cache = passes.AnalysisCache.init(allocator, null);
268 defer cache.deinit();
269 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
270 defer pass_ctx.deinit();
271
272 var state = RecordingBackendState{
273 .allocator = allocator,
274 .kind = .cuda,
275 .format = .cuda_ptx,
276 };
277 const handle = state.handle();
278
279 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
280 defer artifact_plan.deinit();
281 try testing.expectEqual(@as(usize, 2), artifact_plan.kernelCount());
282
283 var executable = try loadKernels(allocator, handle, &artifact_plan);
284 defer executable.deinit();
285
286 const slot_bindings = try slotBindingsForPlan(allocator, &artifact_plan, .cuda);
287 defer allocator.free(slot_bindings);
288 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
289 defer allocator.free(count_bindings);
290
291 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
292 defer graph.deinit();
293 graph.nodes[0].stream = .{ .id = 71, .backend = .cuda };
294 graph.nodes[1].stream = .{ .id = 72, .backend = .cuda };
295
296 try executable.launchGraph(
297 allocator,
298 &artifact_plan,
299 slot_bindings,
300 count_bindings,
301 graph.plan(),
302 );
303
304 try testing.expectEqual(@as(usize, 1), state.created_event_count);
305 const dependency_event = state.created_events[0];
306 try testing.expectEqual(@as(usize, 2), state.launch_count);
307 try testing.expectEqual(@as(gpu.BackendObjectId, 71), state.launch_streams[0].?);
308 try testing.expectEqual(@as(gpu.BackendObjectId, 72), state.launch_streams[1].?);
309 try testing.expectEqual(@as(usize, 1), state.launch_wait_counts[1]);
310 try testing.expectEqual(dependency_event, state.launch_wait_events[1][0]);
311 try testing.expectEqual(@as(usize, 1), state.record_event_count);
312 try testing.expectEqual(@as(gpu.BackendObjectId, 71), state.record_streams[0].?);
313 try testing.expectEqual(dependency_event, state.record_events[0].?);
314 }
315
316 test "Choir executable dependency event lowering requires producer streams" {
317 const allocator = testing.allocator;
318
319 var owned = try escapedTwoKernelChoirModule(allocator, "choir_cuda_executable_launch_graph_event_stream");
320 defer owned.deinit();
321
322 var cache = passes.AnalysisCache.init(allocator, null);
323 defer cache.deinit();
324 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
325 defer pass_ctx.deinit();
326
327 var state = RecordingBackendState{
328 .allocator = allocator,
329 .kind = .cuda,
330 .format = .cuda_ptx,
331 };
332 const handle = state.handle();
333
334 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
335 defer artifact_plan.deinit();
336
337 var executable = try loadKernels(allocator, handle, &artifact_plan);
338 defer executable.deinit();
339
340 const slot_bindings = try slotBindingsForPlan(allocator, &artifact_plan, .cuda);
341 defer allocator.free(slot_bindings);
342
343 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
344 defer graph.deinit();
345 graph.nodes[1].stream = .{ .id = 72, .backend = .cuda };
346
347 try testing.expectError(
348 error.LaunchArgumentMismatch,
349 executable.launchGraphWithDependencyEvents(
350 allocator,
351 &artifact_plan,
352 slot_bindings,
353 &.{},
354 graph.plan(),
355 ),
356 );
357 try testing.expectEqual(@as(usize, 0), state.created_event_count);
358 try testing.expectEqual(@as(usize, 0), state.launch_count);
359 }
360
361 test "Choir executable launch graph validates complete unique kernel nodes before launch" {
362 const allocator = testing.allocator;
363
364 var owned = try escapedTwoKernelChoirModule(allocator, "choir_cuda_executable_launch_graph_invalid");
365 defer owned.deinit();
366
367 var cache = passes.AnalysisCache.init(allocator, null);
368 defer cache.deinit();
369 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
370 defer pass_ctx.deinit();
371
372 var state = RecordingBackendState{
373 .allocator = allocator,
374 .kind = .cuda,
375 .format = .cuda_ptx,
376 };
377 const handle = state.handle();
378
379 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
380 defer artifact_plan.deinit();
381 try testing.expectEqual(@as(usize, 2), artifact_plan.kernelCount());
382
383 var executable = try loadKernels(allocator, handle, &artifact_plan);
384 defer executable.deinit();
385
386 const slot_bindings = try slotBindingsForPlan(allocator, &artifact_plan, .cuda);
387 defer allocator.free(slot_bindings);
388
389 const duplicate_nodes = [_]LaunchGraphNode{
390 .{ .kernel_index = 0 },
391 .{ .kernel_index = 0 },
392 };
393 try testing.expectError(
394 error.InvalidArtifact,
395 executable.launchGraph(
396 allocator,
397 &artifact_plan,
398 slot_bindings,
399 &.{},
400 .{ .nodes = &duplicate_nodes },
401 ),
402 );
403
404 const missing_nodes = [_]LaunchGraphNode{.{ .kernel_index = 0 }};
405 try testing.expectError(
406 error.InvalidArtifact,
407 executable.launchGraph(
408 allocator,
409 &artifact_plan,
410 slot_bindings,
411 &.{},
412 .{ .nodes = &missing_nodes },
413 ),
414 );
415 try testing.expectEqual(@as(usize, 0), state.launch_count);
416 }
417
418 test "Choir executable launch graph validates dependency indices before launch" {
419 const allocator = testing.allocator;
420
421 var owned = try escapedTwoKernelChoirModule(allocator, "choir_cuda_executable_launch_graph_bad_dependency");
422 defer owned.deinit();
423
424 var cache = passes.AnalysisCache.init(allocator, null);
425 defer cache.deinit();
426 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
427 defer pass_ctx.deinit();
428
429 var state = RecordingBackendState{
430 .allocator = allocator,
431 .kind = .cuda,
432 .format = .cuda_ptx,
433 };
434 const handle = state.handle();
435
436 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
437 defer artifact_plan.deinit();
438 try testing.expectEqual(@as(usize, 2), artifact_plan.kernelCount());
439
440 var executable = try loadKernels(allocator, handle, &artifact_plan);
441 defer executable.deinit();
442
443 const slot_bindings = try slotBindingsForPlan(allocator, &artifact_plan, .cuda);
444 defer allocator.free(slot_bindings);
445
446 const nodes = [_]LaunchGraphNode{
447 .{ .kernel_index = 0 },
448 .{ .kernel_index = 1 },
449 };
450 const dependencies = [_]LaunchGraphDependency{.{
451 .producer_node_index = 0,
452 .consumer_node_index = 2,
453 .slot_id = artifact_plan.kernels.items[0].output_slot_id,
454 }};
455
456 try testing.expectError(
457 error.InvalidArtifact,
458 executable.launchGraph(
459 allocator,
460 &artifact_plan,
461 slot_bindings,
462 &.{},
463 .{
464 .nodes = &nodes,
465 .dependencies = &dependencies,
466 },
467 ),
468 );
469
470 const mismatched_dependencies = [_]LaunchGraphDependency{.{
471 .producer_node_index = 0,
472 .consumer_node_index = 1,
473 .slot_id = artifact_plan.kernels.items[1].output_slot_id,
474 }};
475 try testing.expectError(
476 error.InvalidArtifact,
477 executable.launchGraph(
478 allocator,
479 &artifact_plan,
480 slot_bindings,
481 &.{},
482 .{
483 .nodes = &nodes,
484 .dependencies = &mismatched_dependencies,
485 },
486 ),
487 );
488
489 const reversed_nodes = [_]LaunchGraphNode{
490 .{ .kernel_index = 1 },
491 .{ .kernel_index = 0 },
492 };
493 const reversed_dependencies = [_]LaunchGraphDependency{.{
494 .producer_node_index = 1,
495 .consumer_node_index = 0,
496 .slot_id = artifact_plan.kernels.items[0].output_slot_id,
497 }};
498 try testing.expectError(
499 error.InvalidArtifact,
500 executable.launchGraph(
501 allocator,
502 &artifact_plan,
503 slot_bindings,
504 &.{},
505 .{
506 .nodes = &reversed_nodes,
507 .dependencies = &reversed_dependencies,
508 },
509 ),
510 );
511 try testing.expectEqual(@as(usize, 0), state.launch_count);
512 }
513
514 test "Choir executable plan launches fastest measured candidate" {
515 const allocator = testing.allocator;
516
517 var owned = try addChoirModule(allocator, "choir_cuda_executable_measured_candidate");
518 defer owned.deinit();
519
520 var cache = passes.AnalysisCache.init(allocator, null);
521 defer cache.deinit();
522 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
523 defer pass_ctx.deinit();
524
525 var state = RecordingBackendState{
526 .allocator = allocator,
527 .kind = .cuda,
528 .format = .cuda_ptx,
529 };
530 const handle = state.handle();
531
532 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
533 defer artifact_plan.deinit();
534
535 var executable = try loadKernels(allocator, handle, &artifact_plan);
536 defer executable.deinit();
537
538 const kernel = artifact_plan.kernels.items[0];
539 try testing.expect(kernel.launch_resources.candidate_count > 1);
540 const slot_bindings = try slotBindingsForKernel(allocator, kernel, .cuda);
541 defer allocator.free(slot_bindings);
542 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
543 defer allocator.free(count_bindings);
544
545 const measurements = [_]LaunchCandidateMeasurement{
546 .{
547 .kernel_id = kernel.kernel_id,
548 .candidate_index = 0,
549 .median_ns = 500,
550 .sample_count = 8,
551 },
552 .{
553 .kernel_id = kernel.kernel_id,
554 .candidate_index = 1,
555 .median_ns = 200,
556 .sample_count = 4,
557 },
558 };
559
560 try executable.launchAllWithOptions(allocator, &artifact_plan, slot_bindings, count_bindings, .{
561 .tuning = .{ .measurements = &measurements },
562 });
563
564 const selected = kernel.launch_resources.candidates[1];
565 try testing.expectEqual(@as(usize, 1), state.launch_count);
566 try testing.expectEqual(selected.geometry.grid[0], state.last_launch_grid[0]);
567 try testing.expectEqual(selected.geometry.threadgroup[0], state.last_launch_threadgroup[0]);
568 }
569
570 test "Choir executable launch tuning selections override measurements" {
571 const allocator = testing.allocator;
572
573 var owned = try addChoirModule(allocator, "choir_cuda_executable_selected_candidate");
574 defer owned.deinit();
575
576 var cache = passes.AnalysisCache.init(allocator, null);
577 defer cache.deinit();
578 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
579 defer pass_ctx.deinit();
580
581 var state = RecordingBackendState{
582 .allocator = allocator,
583 .kind = .cuda,
584 .format = .cuda_ptx,
585 };
586 const handle = state.handle();
587
588 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
589 defer artifact_plan.deinit();
590
591 var executable = try loadKernels(allocator, handle, &artifact_plan);
592 defer executable.deinit();
593
594 const kernel = artifact_plan.kernels.items[0];
595 try testing.expect(kernel.launch_resources.candidate_count > 1);
596 const slot_bindings = try slotBindingsForKernel(allocator, kernel, .cuda);
597 defer allocator.free(slot_bindings);
598 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
599 defer allocator.free(count_bindings);
600
601 const measurements = [_]LaunchCandidateMeasurement{.{
602 .kernel_id = kernel.kernel_id,
603 .candidate_index = 0,
604 .median_ns = 1,
605 .sample_count = 8,
606 }};
607 const selections = [_]LaunchTuningSelection{.{
608 .kernel_id = kernel.kernel_id,
609 .candidate_index = 1,
610 .median_ns = 200,
611 .sample_count = 4,
612 }};
613
614 try executable.launchAllWithOptions(allocator, &artifact_plan, slot_bindings, count_bindings, .{
615 .tuning = .{
616 .selections = &selections,
617 .measurements = &measurements,
618 },
619 });
620
621 const selected = kernel.launch_resources.candidates[1];
622 try testing.expectEqual(@as(usize, 1), state.launch_count);
623 try testing.expectEqual(selected.geometry.grid[0], state.last_launch_grid[0]);
624 try testing.expectEqual(selected.geometry.threadgroup[0], state.last_launch_threadgroup[0]);
625
626 const duplicate_selections = [_]LaunchTuningSelection{ selections[0], selections[0] };
627 try testing.expectError(
628 error.LaunchArgumentMismatch,
629 executable.launchAllWithOptions(allocator, &artifact_plan, slot_bindings, count_bindings, .{
630 .tuning = .{ .selections = &duplicate_selections },
631 }),
632 );
633 try testing.expectEqual(@as(usize, 1), state.launch_count);
634 }
635
636 test "Choir executable launch graph persists measured tuning selections" {
637 const allocator = testing.allocator;
638
639 var owned = try addChoirModule(allocator, "choir_cuda_executable_graph_tuning");
640 defer owned.deinit();
641
642 var cache = passes.AnalysisCache.init(allocator, null);
643 defer cache.deinit();
644 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
645 defer pass_ctx.deinit();
646
647 var state = RecordingBackendState{
648 .allocator = allocator,
649 .kind = .cuda,
650 .format = .cuda_ptx,
651 };
652 const handle = state.handle();
653
654 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
655 defer artifact_plan.deinit();
656
657 var executable = try loadKernels(allocator, handle, &artifact_plan);
658 defer executable.deinit();
659
660 const kernel = artifact_plan.kernels.items[0];
661 try testing.expect(kernel.launch_resources.candidate_count > 1);
662 const slot_bindings = try slotBindingsForKernel(allocator, kernel, .cuda);
663 defer allocator.free(slot_bindings);
664 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
665 defer allocator.free(count_bindings);
666
667 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
668 defer graph.deinit();
669
670 const measurements = [_]LaunchCandidateMeasurement{
671 .{
672 .kernel_id = kernel.kernel_id,
673 .candidate_index = 0,
674 .median_ns = 500,
675 .sample_count = 8,
676 },
677 .{
678 .kernel_id = kernel.kernel_id,
679 .candidate_index = 1,
680 .median_ns = 200,
681 .sample_count = 4,
682 },
683 };
684
685 try graph.applyMeasuredLaunchTuning(&artifact_plan, &measurements);
686
687 try testing.expectEqual(@as(usize, 1), graph.tuning_selections.len);
688 try testing.expectEqual(kernel.kernel_id, graph.tuning_selections[0].kernel_id);
689 try testing.expectEqual(@as(usize, 1), graph.tuning_selections[0].candidate_index);
690 try testing.expectEqual(@as(u64, 200), graph.tuning_selections[0].median_ns);
691 try testing.expectEqual(@as(u32, 4), graph.tuning_selections[0].sample_count);
692 try testing.expectEqual(@as(usize, 1), graph.nodes[0].tuning.selections.len);
693 try testing.expectEqual(@as(usize, 0), graph.nodes[0].tuning.measurements.len);
694 try testing.expectEqual(@intFromPtr(graph.tuning_selections.ptr), @intFromPtr(graph.nodes[0].tuning.selections.ptr));
695
696 try executable.launchGraph(
697 allocator,
698 &artifact_plan,
699 slot_bindings,
700 count_bindings,
701 graph.plan(),
702 );
703
704 const selected = kernel.launch_resources.candidates[1];
705 try testing.expectEqual(@as(usize, 1), state.launch_count);
706 try testing.expectEqual(selected.geometry.grid[0], state.last_launch_grid[0]);
707 try testing.expectEqual(selected.geometry.threadgroup[0], state.last_launch_threadgroup[0]);
708 }
709
710 test "Choir executable launch tuning cache applies measured graph selections" {
711 const allocator = testing.allocator;
712
713 var owned = try addChoirModule(allocator, "choir_cuda_executable_graph_tuning_cache");
714 defer owned.deinit();
715
716 var cache = passes.AnalysisCache.init(allocator, null);
717 defer cache.deinit();
718 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
719 defer pass_ctx.deinit();
720
721 var state = RecordingBackendState{
722 .allocator = allocator,
723 .kind = .cuda,
724 .format = .cuda_ptx,
725 };
726 const handle = state.handle();
727 const caps = try handle.queryCapabilities();
728
729 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
730 defer artifact_plan.deinit();
731
732 var executable = try loadKernels(allocator, handle, &artifact_plan);
733 defer executable.deinit();
734
735 const kernel = artifact_plan.kernels.items[0];
736 try testing.expect(kernel.launch_resources.candidate_count > 1);
737 const slot_bindings = try slotBindingsForKernel(allocator, kernel, .cuda);
738 defer allocator.free(slot_bindings);
739 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
740 defer allocator.free(count_bindings);
741
742 var tuning_cache = LaunchTuningCache.init(allocator);
743 defer tuning_cache.deinit();
744
745 const measurements = [_]LaunchCandidateMeasurement{
746 .{
747 .kernel_id = kernel.kernel_id,
748 .candidate_index = 0,
749 .median_ns = 500,
750 .sample_count = 8,
751 },
752 .{
753 .kernel_id = kernel.kernel_id,
754 .candidate_index = 1,
755 .median_ns = 200,
756 .sample_count = 4,
757 },
758 };
759 try tuning_cache.recordMeasurements(caps, &artifact_plan, &measurements);
760 try testing.expectEqual(@as(usize, 1), tuning_cache.count());
761
762 const slower_measurements = [_]LaunchCandidateMeasurement{.{
763 .kernel_id = kernel.kernel_id,
764 .candidate_index = 0,
765 .median_ns = 600,
766 .sample_count = 16,
767 }};
768 try tuning_cache.recordMeasurements(caps, &artifact_plan, &slower_measurements);
769 try testing.expectEqual(@as(usize, 1), tuning_cache.count());
770
771 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
772 defer graph.deinit();
773 try graph.applyCachedLaunchTuning(caps, &artifact_plan, &tuning_cache);
774
775 try testing.expectEqual(@as(usize, 1), graph.tuning_selections.len);
776 try testing.expectEqual(kernel.kernel_id, graph.tuning_selections[0].kernel_id);
777 try testing.expectEqual(@as(usize, 1), graph.tuning_selections[0].candidate_index);
778 try testing.expectEqual(@as(u64, 200), graph.tuning_selections[0].median_ns);
779 try testing.expectEqual(@as(u32, 4), graph.tuning_selections[0].sample_count);
780 try testing.expectEqual(@as(usize, 1), graph.nodes[0].tuning.selections.len);
781 try testing.expectEqual(@as(usize, 0), graph.nodes[0].tuning.measurements.len);
782
783 try executable.launchGraph(
784 allocator,
785 &artifact_plan,
786 slot_bindings,
787 count_bindings,
788 graph.plan(),
789 );
790
791 const selected = kernel.launch_resources.candidates[1];
792 try testing.expectEqual(@as(usize, 1), state.launch_count);
793 try testing.expectEqual(selected.geometry.grid[0], state.last_launch_grid[0]);
794 try testing.expectEqual(selected.geometry.threadgroup[0], state.last_launch_threadgroup[0]);
795 }
796
797 test "Choir executable launch tuning cache exports and imports records" {
798 const allocator = testing.allocator;
799
800 var owned = try addChoirModule(allocator, "choir_cuda_executable_graph_tuning_records");
801 defer owned.deinit();
802
803 var cache = passes.AnalysisCache.init(allocator, null);
804 defer cache.deinit();
805 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
806 defer pass_ctx.deinit();
807
808 var state = RecordingBackendState{
809 .allocator = allocator,
810 .kind = .cuda,
811 .format = .cuda_ptx,
812 };
813 const handle = state.handle();
814 const caps = try handle.queryCapabilities();
815
816 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
817 defer artifact_plan.deinit();
818
819 var executable = try loadKernels(allocator, handle, &artifact_plan);
820 defer executable.deinit();
821
822 const kernel = artifact_plan.kernels.items[0];
823 try testing.expect(kernel.launch_resources.candidate_count > 1);
824 const slot_bindings = try slotBindingsForKernel(allocator, kernel, .cuda);
825 defer allocator.free(slot_bindings);
826 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
827 defer allocator.free(count_bindings);
828
829 var tuning_cache = LaunchTuningCache.init(allocator);
830 defer tuning_cache.deinit();
831
832 const measurements = [_]LaunchCandidateMeasurement{
833 .{
834 .kernel_id = kernel.kernel_id,
835 .candidate_index = 0,
836 .median_ns = 500,
837 .sample_count = 8,
838 },
839 .{
840 .kernel_id = kernel.kernel_id,
841 .candidate_index = 1,
842 .median_ns = 200,
843 .sample_count = 4,
844 },
845 };
846 try tuning_cache.recordMeasurements(caps, &artifact_plan, &measurements);
847
848 const records = try tuning_cache.exportRecords(allocator);
849 defer allocator.free(records);
850 try testing.expectEqual(@as(usize, 1), records.len);
851 try testing.expectEqual(launch_tuning_cache_record_version, records[0].version);
852 try testing.expectEqual(@as(u32, @intCast(kernel.launch_resources.candidate_count)), records[0].key.candidate_count);
853 try testing.expect(records[0].key.launch_resource_fingerprint != 0);
854 try testing.expectEqual(kernel.output_layout_fingerprint, records[0].key.output_layout_fingerprint);
855 try testing.expectEqual(kernel.input_layout_fingerprint, records[0].key.input_layout_fingerprint);
856 try testing.expectEqual(kernel.kernel_id, records[0].selection.kernel_id);
857 try testing.expectEqual(@as(usize, 1), records[0].selection.candidate_index);
858
859 var imported = LaunchTuningCache.init(allocator);
860 defer imported.deinit();
861 try imported.importRecords(records);
862 try testing.expectEqual(@as(usize, 1), imported.count());
863
864 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
865 defer graph.deinit();
866 try graph.applyCachedLaunchTuning(caps, &artifact_plan, &imported);
867
868 try testing.expectEqual(@as(usize, 1), graph.tuning_selections.len);
869 try testing.expectEqual(kernel.kernel_id, graph.tuning_selections[0].kernel_id);
870 try testing.expectEqual(@as(usize, 1), graph.tuning_selections[0].candidate_index);
871 try testing.expectEqual(@as(u64, 200), graph.tuning_selections[0].median_ns);
872
873 try executable.launchGraph(
874 allocator,
875 &artifact_plan,
876 slot_bindings,
877 count_bindings,
878 graph.plan(),
879 );
880
881 const selected = kernel.launch_resources.candidates[1];
882 try testing.expectEqual(@as(usize, 1), state.launch_count);
883 try testing.expectEqual(selected.geometry.grid[0], state.last_launch_grid[0]);
884 try testing.expectEqual(selected.geometry.threadgroup[0], state.last_launch_threadgroup[0]);
885 }
886
887 test "Choir executable launch tuning artifact round-trips records" {
888 const allocator = testing.allocator;
889
890 var owned = try addChoirModule(allocator, "choir_cuda_executable_graph_tuning_artifact");
891 defer owned.deinit();
892
893 var cache = passes.AnalysisCache.init(allocator, null);
894 defer cache.deinit();
895 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
896 defer pass_ctx.deinit();
897
898 var state = RecordingBackendState{
899 .allocator = allocator,
900 .kind = .cuda,
901 .format = .cuda_ptx,
902 };
903 const handle = state.handle();
904 const caps = try handle.queryCapabilities();
905
906 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
907 defer artifact_plan.deinit();
908
909 var executable = try loadKernels(allocator, handle, &artifact_plan);
910 defer executable.deinit();
911
912 const kernel = artifact_plan.kernels.items[0];
913 try testing.expect(kernel.launch_resources.candidate_count > 1);
914 const slot_bindings = try slotBindingsForKernel(allocator, kernel, .cuda);
915 defer allocator.free(slot_bindings);
916 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
917 defer allocator.free(count_bindings);
918
919 var tuning_cache = LaunchTuningCache.init(allocator);
920 defer tuning_cache.deinit();
921
922 const measurements = [_]LaunchCandidateMeasurement{
923 .{
924 .kernel_id = kernel.kernel_id,
925 .candidate_index = 0,
926 .median_ns = 500,
927 .sample_count = 8,
928 },
929 .{
930 .kernel_id = kernel.kernel_id,
931 .candidate_index = 1,
932 .median_ns = 200,
933 .sample_count = 4,
934 },
935 };
936 try tuning_cache.recordMeasurements(caps, &artifact_plan, &measurements);
937
938 const records = try tuning_cache.exportRecords(allocator);
939 defer allocator.free(records);
940 const artifact_bytes = try encodeLaunchTuningArtifact(allocator, records);
941 defer allocator.free(artifact_bytes);
942 const decoded = try decodeLaunchTuningArtifact(allocator, artifact_bytes);
943 defer allocator.free(decoded);
944 const encoded_again = try encodeLaunchTuningArtifact(allocator, decoded);
945 defer allocator.free(encoded_again);
946
947 try testing.expectEqualSlices(u8, artifact_bytes, encoded_again);
948 try testing.expectEqual(@as(usize, 1), decoded.len);
949 try testing.expectEqual(launch_tuning_cache_record_version, decoded[0].version);
950 try testing.expectEqual(records[0].key, decoded[0].key);
951 try testing.expectEqual(kernel.output_layout_fingerprint, decoded[0].key.output_layout_fingerprint);
952 try testing.expectEqual(kernel.input_layout_fingerprint, decoded[0].key.input_layout_fingerprint);
953 try testing.expectEqual(records[0].selection, decoded[0].selection);
954
955 var imported = LaunchTuningCache.init(allocator);
956 defer imported.deinit();
957 try imported.importRecords(decoded);
958
959 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
960 defer graph.deinit();
961 try graph.applyCachedLaunchTuning(caps, &artifact_plan, &imported);
962
963 try testing.expectEqual(@as(usize, 1), graph.tuning_selections.len);
964 try testing.expectEqual(kernel.kernel_id, graph.tuning_selections[0].kernel_id);
965 try testing.expectEqual(@as(usize, 1), graph.tuning_selections[0].candidate_index);
966 try testing.expectEqual(@as(u64, 200), graph.tuning_selections[0].median_ns);
967
968 try executable.launchGraph(
969 allocator,
970 &artifact_plan,
971 slot_bindings,
972 count_bindings,
973 graph.plan(),
974 );
975
976 const selected = kernel.launch_resources.candidates[1];
977 try testing.expectEqual(@as(usize, 1), state.launch_count);
978 try testing.expectEqual(selected.geometry.grid[0], state.last_launch_grid[0]);
979 try testing.expectEqual(selected.geometry.threadgroup[0], state.last_launch_threadgroup[0]);
980 }
981
982 test "Choir executable launch tuning artifact rejects invalid bytes" {
983 const allocator = testing.allocator;
984 const record = LaunchTuningCacheRecord{
985 .key = .{
986 .backend = .cuda,
987 .family = .nvidia_cuda,
988 .format = .cuda_ptx,
989 .candidate_count = 2,
990 },
991 .selection = .{
992 .kernel_id = 0,
993 .candidate_index = 1,
994 .median_ns = 200,
995 .sample_count = 4,
996 },
997 };
998 const artifact_bytes = try encodeLaunchTuningArtifact(allocator, &.{record});
999 defer allocator.free(artifact_bytes);
1000
1001 var bad_magic = try allocator.dupe(u8, artifact_bytes);
1002 defer allocator.free(bad_magic);
1003 bad_magic[0] ^= 0xff;
1004 try testing.expectError(error.InvalidArtifact, decodeLaunchTuningArtifact(allocator, bad_magic));
1005
1006 var bad_version = try allocator.dupe(u8, artifact_bytes);
1007 defer allocator.free(bad_version);
1008 bad_version[4] ^= 0xff;
1009 try testing.expectError(error.InvalidArtifact, decodeLaunchTuningArtifact(allocator, bad_version));
1010
1011 try testing.expectError(error.InvalidArtifact, decodeLaunchTuningArtifact(allocator, artifact_bytes[0 .. artifact_bytes.len - 1]));
1012
1013 var extra = std.ArrayListUnmanaged(u8).empty;
1014 defer extra.deinit(allocator);
1015 try extra.appendSlice(allocator, artifact_bytes);
1016 try extra.append(allocator, 0);
1017 try testing.expectError(error.InvalidArtifact, decodeLaunchTuningArtifact(allocator, extra.items));
1018 }
1019
1020 test "Choir executable launch tuning cache rejects invalid records before import" {
1021 const allocator = testing.allocator;
1022
1023 var owned = try addChoirModule(allocator, "choir_cuda_executable_graph_tuning_bad_records");
1024 defer owned.deinit();
1025
1026 var cache = passes.AnalysisCache.init(allocator, null);
1027 defer cache.deinit();
1028 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
1029 defer pass_ctx.deinit();
1030
1031 var state = RecordingBackendState{
1032 .allocator = allocator,
1033 .kind = .cuda,
1034 .format = .cuda_ptx,
1035 };
1036 const handle = state.handle();
1037 const caps = try handle.queryCapabilities();
1038
1039 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
1040 defer artifact_plan.deinit();
1041
1042 const kernel = artifact_plan.kernels.items[0];
1043 const key = LaunchTuningCacheKey.init(caps, kernel);
1044 const valid = LaunchTuningCacheRecord{
1045 .key = key,
1046 .selection = .{
1047 .kernel_id = kernel.kernel_id,
1048 .candidate_index = 0,
1049 .median_ns = 500,
1050 .sample_count = 8,
1051 },
1052 };
1053 const invalid = LaunchTuningCacheRecord{
1054 .key = key,
1055 .selection = .{
1056 .kernel_id = kernel.kernel_id,
1057 .candidate_index = @intCast(key.candidate_count),
1058 .median_ns = 1,
1059 .sample_count = 1,
1060 },
1061 };
1062 const records = [_]LaunchTuningCacheRecord{ valid, invalid };
1063
1064 var tuning_cache = LaunchTuningCache.init(allocator);
1065 defer tuning_cache.deinit();
1066 try testing.expectError(error.LaunchArgumentMismatch, tuning_cache.importRecords(&records));
1067 try testing.expectEqual(@as(usize, 0), tuning_cache.count());
1068
1069 try tuning_cache.importRecords(&.{valid});
1070 try testing.expectEqual(@as(usize, 1), tuning_cache.count());
1071
1072 var stale = valid;
1073 stale.version = launch_tuning_cache_record_version + 1;
1074 try testing.expectError(error.InvalidArtifact, tuning_cache.importRecords(&.{stale}));
1075 try testing.expectEqual(@as(usize, 1), tuning_cache.count());
1076 }
1077
1078 test "Choir executable launch tuning cache keys reuse by launch resource identity" {
1079 const allocator = testing.allocator;
1080
1081 var owned = try addChoirModule(allocator, "choir_cuda_executable_graph_tuning_cache_key");
1082 defer owned.deinit();
1083
1084 var cache = passes.AnalysisCache.init(allocator, null);
1085 defer cache.deinit();
1086 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
1087 defer pass_ctx.deinit();
1088
1089 var state = RecordingBackendState{
1090 .allocator = allocator,
1091 .kind = .cuda,
1092 .format = .cuda_ptx,
1093 };
1094 const handle = state.handle();
1095 const caps = try handle.queryCapabilities();
1096
1097 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
1098 defer artifact_plan.deinit();
1099
1100 const kernel = artifact_plan.kernels.items[0];
1101 try testing.expect(kernel.launch_resources.candidate_count > 1);
1102
1103 var tuning_cache = LaunchTuningCache.init(allocator);
1104 defer tuning_cache.deinit();
1105 try tuning_cache.recordMeasuredSelection(caps, kernel, .{
1106 .kernel_id = kernel.kernel_id,
1107 .candidate_index = 1,
1108 .median_ns = 200,
1109 .sample_count = 4,
1110 });
1111
1112 var equivalent_kernel = kernel;
1113 equivalent_kernel.kernel_id += 1000;
1114 const cached = (try tuning_cache.selectionForKernel(caps, equivalent_kernel)).?;
1115 try testing.expectEqual(equivalent_kernel.kernel_id, cached.kernel_id);
1116 try testing.expectEqual(@as(usize, 1), cached.candidate_index);
1117
1118 var different_caps = caps;
1119 different_caps.threadgroup.max_threads = 512;
1120 try testing.expectEqual(@as(?LaunchTuningSelection, null), try tuning_cache.selectionForKernel(different_caps, kernel));
1121 }
1122
1123 test "Choir executable launch graph rejects invalid measured tuning selection" {
1124 const allocator = testing.allocator;
1125
1126 var owned = try addChoirModule(allocator, "choir_cuda_executable_graph_bad_tuning");
1127 defer owned.deinit();
1128
1129 var cache = passes.AnalysisCache.init(allocator, null);
1130 defer cache.deinit();
1131 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
1132 defer pass_ctx.deinit();
1133
1134 var state = RecordingBackendState{
1135 .allocator = allocator,
1136 .kind = .cuda,
1137 .format = .cuda_ptx,
1138 };
1139 const handle = state.handle();
1140
1141 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
1142 defer artifact_plan.deinit();
1143
1144 const kernel = artifact_plan.kernels.items[0];
1145 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
1146 defer graph.deinit();
1147
1148 const measurements = [_]LaunchCandidateMeasurement{.{
1149 .kernel_id = kernel.kernel_id,
1150 .candidate_index = kernel.launch_resources.candidate_count,
1151 .median_ns = 1,
1152 }};
1153
1154 try testing.expectError(
1155 error.LaunchArgumentMismatch,
1156 graph.applyMeasuredLaunchTuning(&artifact_plan, &measurements),
1157 );
1158 try testing.expectEqual(@as(usize, 0), graph.tuning_selections.len);
1159 try testing.expectEqual(@as(usize, 0), graph.nodes[0].tuning.selections.len);
1160 try testing.expectEqual(@as(usize, 0), graph.nodes[0].tuning.measurements.len);
1161 }
1162
1163 test "Choir executable plan rejects invalid measured candidate" {
1164 const allocator = testing.allocator;
1165
1166 var owned = try addChoirModule(allocator, "choir_cuda_executable_invalid_candidate");
1167 defer owned.deinit();
1168
1169 var cache = passes.AnalysisCache.init(allocator, null);
1170 defer cache.deinit();
1171 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
1172 defer pass_ctx.deinit();
1173
1174 var state = RecordingBackendState{
1175 .allocator = allocator,
1176 .kind = .cuda,
1177 .format = .cuda_ptx,
1178 };
1179 const handle = state.handle();
1180
1181 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
1182 defer artifact_plan.deinit();
1183
1184 var executable = try loadKernels(allocator, handle, &artifact_plan);
1185 defer executable.deinit();
1186
1187 const kernel = artifact_plan.kernels.items[0];
1188 const slot_bindings = try slotBindingsForKernel(allocator, kernel, .cuda);
1189 defer allocator.free(slot_bindings);
1190
1191 const measurements = [_]LaunchCandidateMeasurement{.{
1192 .kernel_id = kernel.kernel_id,
1193 .candidate_index = kernel.launch_resources.candidate_count,
1194 .median_ns = 1,
1195 }};
1196
1197 try testing.expectError(
1198 error.LaunchArgumentMismatch,
1199 executable.launchAllWithOptions(allocator, &artifact_plan, slot_bindings, &.{}, .{
1200 .tuning = .{ .measurements = &measurements },
1201 }),
1202 );
1203 try testing.expectEqual(@as(usize, 0), state.launch_count);
1204 }
1205
1206 test "Choir executable plan measures launch candidates" {
1207 const allocator = testing.allocator;
1208
1209 var owned = try addChoirModule(allocator, "choir_cuda_executable_measure_candidates");
1210 defer owned.deinit();
1211
1212 var cache = passes.AnalysisCache.init(allocator, null);
1213 defer cache.deinit();
1214 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
1215 defer pass_ctx.deinit();
1216
1217 var state = RecordingBackendState{
1218 .allocator = allocator,
1219 .kind = .cuda,
1220 .format = .cuda_ptx,
1221 };
1222 const handle = state.handle();
1223
1224 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
1225 defer artifact_plan.deinit();
1226
1227 var executable = try loadKernels(allocator, handle, &artifact_plan);
1228 defer executable.deinit();
1229
1230 const kernel = artifact_plan.kernels.items[0];
1231 try testing.expect(kernel.launch_resources.candidate_count > 1);
1232 const slot_bindings = try slotBindingsForKernel(allocator, kernel, .cuda);
1233 defer allocator.free(slot_bindings);
1234 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
1235 defer allocator.free(count_bindings);
1236
1237 const measurements = try executable.measureLaunchCandidates(
1238 allocator,
1239 allocator,
1240 &artifact_plan,
1241 0,
1242 slot_bindings,
1243 firstElementCountBinding(count_bindings),
1244 .{ .warmup = 1, .samples = 2 },
1245 );
1246 defer allocator.free(measurements);
1247
1248 try testing.expectEqual(kernel.launch_resources.candidate_count, measurements.len);
1249 try testing.expectEqual(kernel.launch_resources.candidate_count * 3, state.launch_count);
1250 for (measurements, 0..) |measurement, index| {
1251 try testing.expectEqual(kernel.kernel_id, measurement.kernel_id);
1252 try testing.expectEqual(index, measurement.candidate_index);
1253 try testing.expectEqual(@as(u32, 2), measurement.sample_count);
1254 }
1255 }
1256
1257 test "Choir executable launch candidate measurement requires samples" {
1258 const allocator = testing.allocator;
1259
1260 var owned = try addChoirModule(allocator, "choir_cuda_executable_measure_no_samples");
1261 defer owned.deinit();
1262
1263 var cache = passes.AnalysisCache.init(allocator, null);
1264 defer cache.deinit();
1265 var pass_ctx = passes.PassContext.init(owned.choir_module, owned.ctx, allocator, &cache);
1266 defer pass_ctx.deinit();
1267
1268 var state = RecordingBackendState{
1269 .allocator = allocator,
1270 .kind = .cuda,
1271 .format = .cuda_ptx,
1272 };
1273 const handle = state.handle();
1274
1275 var artifact_plan = try createTestBackendArtifactPlan(allocator, handle, &pass_ctx, owned.choir_module, .{});
1276 defer artifact_plan.deinit();
1277
1278 var executable = try loadKernels(allocator, handle, &artifact_plan);
1279 defer executable.deinit();
1280
1281 const kernel = artifact_plan.kernels.items[0];
1282 const slot_bindings = try slotBindingsForKernel(allocator, kernel, .cuda);
1283 defer allocator.free(slot_bindings);
1284 const count_bindings = try elementCountBindingsForPlan(allocator, &artifact_plan, .cuda);
1285 defer allocator.free(count_bindings);
1286
1287 try testing.expectError(
1288 error.LaunchArgumentMismatch,
1289 executable.measureLaunchCandidates(
1290 allocator,
1291 allocator,
1292 &artifact_plan,
1293 0,
1294 slot_bindings,
1295 firstElementCountBinding(count_bindings),
1296 .{ .samples = 0 },
1297 ),
1298 );
1299 try testing.expectEqual(@as(usize, 0), state.launch_count);
1300 }