lib/accy/src/executable/plan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gpu = @import("gpu");
3 const choir_abi = @import("choir_abi");
4 const choir = @import("choir");
5 const accy_root = @import("../root.zig");
6 const artifact_product = @import("../artifact/root.zig");
7 const tuning_mod = @import("tuning.zig");
8
9 pub const product_name = "accy.exec";
10
11 pub const LaunchOptions = struct {
12 stream: ?gpu.StreamHandle = null,
13 wait_events: []const gpu.EventHandle = &.{},
14 signal_event: ?gpu.EventHandle = null,
15 tuning: LaunchTuning = .{},
16 runtime_scalar_arguments: []const choir_abi.ScalarArgument = &.{},
17 };
18
19 pub const LaunchGraphNode = struct {
20 kernel_index: usize,
21 stream: ?gpu.StreamHandle = null,
22 wait_events: []const gpu.EventHandle = &.{},
23 signal_event: ?gpu.EventHandle = null,
24 tuning: LaunchTuning = .{},
25 runtime_scalar_arguments: []const choir_abi.ScalarArgument = &.{},
26 };
27
28 pub const LaunchGraphDependency = struct {
29 producer_node_index: usize,
30 consumer_node_index: usize,
31 slot_id: usize,
32 };
33
34 pub const LaunchGraphLoopCarry = struct {
35 initial_slot_id: usize,
36 input_slot_id: usize,
37 output_slot_id: usize,
38 final_slot_id: usize,
39 };
40
41 pub const LaunchGraphLoop = struct {
42 first_node_index: usize,
43 node_count: usize,
44 trip_count: u64,
45 carries: []const LaunchGraphLoopCarry = &.{},
46 };
47
48 pub fn launchGraphLoopCarryFinalSlot(carry: LaunchGraphLoopCarry, trip_count: u64) usize {
49 if (trip_count % 2 == 0) return carry.initial_slot_id;
50 return carry.output_slot_id;
51 }
52
53 pub const LaunchGraphPlan = struct {
54 nodes: []const LaunchGraphNode,
55 dependencies: []const LaunchGraphDependency = &.{},
56 loops: []const LaunchGraphLoop = &.{},
57 validated: bool = false,
58 };
59
60 pub const OwnedLaunchGraphPlan = struct {
61 allocator: std.mem.Allocator,
62 nodes: []LaunchGraphNode,
63 dependencies: []LaunchGraphDependency,
64 loops: []LaunchGraphLoop = &.{},
65 tuning_selections: []LaunchTuningSelection = &.{},
66 validated: bool = false,
67
68 pub fn deinit(self: *OwnedLaunchGraphPlan) void {
69 if (self.nodes.len != 0) self.allocator.free(self.nodes);
70 if (self.dependencies.len != 0) self.allocator.free(self.dependencies);
71 for (self.loops) |loop| {
72 if (loop.carries.len != 0) self.allocator.free(@constCast(loop.carries));
73 }
74 if (self.loops.len != 0) self.allocator.free(self.loops);
75 if (self.tuning_selections.len != 0) self.allocator.free(self.tuning_selections);
76 self.* = undefined;
77 }
78
79 pub fn plan(self: *const OwnedLaunchGraphPlan) LaunchGraphPlan {
80 return .{
81 .nodes = self.nodes,
82 .dependencies = self.dependencies,
83 .loops = self.loops,
84 .validated = self.validated,
85 };
86 }
87
88 pub fn copyWithLaunchOptions(
89 self: *const OwnedLaunchGraphPlan,
90 allocator: std.mem.Allocator,
91 launch_options: LaunchOptions,
92 ) gpu.BackendError!OwnedLaunchGraphPlan {
93 const nodes = allocator.alloc(LaunchGraphNode, self.nodes.len) catch return error.OutOfMemory;
94 errdefer allocator.free(nodes);
95
96 for (nodes, 0..) |*node, index| {
97 node.* = self.nodes[index];
98 node.stream = launch_options.stream;
99 node.wait_events = if (index == 0) launch_options.wait_events else &.{};
100 node.signal_event = if (index + 1 == nodes.len) launch_options.signal_event else null;
101 node.tuning = launch_options.tuning;
102 node.runtime_scalar_arguments = launch_options.runtime_scalar_arguments;
103 }
104
105 var dependencies: []LaunchGraphDependency = &.{};
106 if (self.dependencies.len != 0) {
107 dependencies = allocator.dupe(LaunchGraphDependency, self.dependencies) catch return error.OutOfMemory;
108 }
109 errdefer if (dependencies.len != 0) allocator.free(dependencies);
110
111 var loops: []LaunchGraphLoop = &.{};
112 if (self.loops.len != 0) {
113 loops = allocator.alloc(LaunchGraphLoop, self.loops.len) catch return error.OutOfMemory;
114 var copied_loops: usize = 0;
115 errdefer {
116 for (loops[0..copied_loops]) |loop| {
117 if (loop.carries.len != 0) allocator.free(@constCast(loop.carries));
118 }
119 allocator.free(loops);
120 }
121 for (self.loops, 0..) |loop, index| {
122 var carries: []LaunchGraphLoopCarry = &.{};
123 if (loop.carries.len != 0) {
124 carries = allocator.dupe(LaunchGraphLoopCarry, loop.carries) catch return error.OutOfMemory;
125 }
126 loops[index] = .{
127 .first_node_index = loop.first_node_index,
128 .node_count = loop.node_count,
129 .trip_count = loop.trip_count,
130 .carries = carries,
131 };
132 copied_loops += 1;
133 }
134 }
135
136 return .{
137 .allocator = allocator,
138 .nodes = nodes,
139 .dependencies = dependencies,
140 .loops = loops,
141 };
142 }
143
144 pub fn applyMeasuredLaunchTuning(
145 self: *OwnedLaunchGraphPlan,
146 artifact_plan: *const artifact_product.BackendArtifactPlan,
147 measurements: []const LaunchCandidateMeasurement,
148 ) gpu.BackendError!void {
149 if (self.nodes.len != artifact_plan.kernels.items.len) return error.InvalidArtifact;
150
151 var selection_count: usize = 0;
152 for (self.nodes) |node| {
153 if (node.kernel_index >= artifact_plan.kernels.items.len) return error.InvalidArtifact;
154 const planned = artifact_plan.kernels.items[node.kernel_index];
155 if (try selectedLaunchMeasurement(planned, measurements)) |_| selection_count += 1;
156 }
157
158 var new_selections: []LaunchTuningSelection = &.{};
159 if (selection_count != 0) {
160 new_selections = self.allocator.alloc(LaunchTuningSelection, selection_count) catch return error.OutOfMemory;
161 }
162 errdefer if (new_selections.len != 0) self.allocator.free(new_selections);
163
164 var selection_index: usize = 0;
165 for (self.nodes) |node| {
166 const planned = artifact_plan.kernels.items[node.kernel_index];
167 if (try selectedLaunchMeasurement(planned, measurements)) |measurement| {
168 new_selections[selection_index] = .{
169 .kernel_id = measurement.kernel_id,
170 .candidate_index = measurement.candidate_index,
171 .median_ns = measurement.median_ns,
172 .sample_count = measurement.sample_count,
173 };
174 selection_index += 1;
175 }
176 }
177
178 self.installLaunchTuningSelections(artifact_plan, new_selections);
179 }
180
181 pub fn applyCachedLaunchTuning(
182 self: *OwnedLaunchGraphPlan,
183 caps: gpu.BackendCapabilities,
184 artifact_plan: *const artifact_product.BackendArtifactPlan,
185 cache: *const LaunchTuningCache,
186 ) gpu.BackendError!void {
187 if (self.nodes.len != artifact_plan.kernels.items.len) return error.InvalidArtifact;
188
189 var selection_count: usize = 0;
190 for (self.nodes) |node| {
191 if (node.kernel_index >= artifact_plan.kernels.items.len) return error.InvalidArtifact;
192 const planned = artifact_plan.kernels.items[node.kernel_index];
193 if (try cache.selectionForKernel(caps, planned)) |_| selection_count += 1;
194 }
195
196 var new_selections: []LaunchTuningSelection = &.{};
197 if (selection_count != 0) {
198 new_selections = self.allocator.alloc(LaunchTuningSelection, selection_count) catch return error.OutOfMemory;
199 }
200 errdefer if (new_selections.len != 0) self.allocator.free(new_selections);
201
202 var selection_index: usize = 0;
203 for (self.nodes) |node| {
204 const planned = artifact_plan.kernels.items[node.kernel_index];
205 if (try cache.selectionForKernel(caps, planned)) |selection| {
206 new_selections[selection_index] = selection;
207 selection_index += 1;
208 }
209 }
210
211 self.installLaunchTuningSelections(artifact_plan, new_selections);
212 }
213
214 fn installLaunchTuningSelections(
215 self: *OwnedLaunchGraphPlan,
216 artifact_plan: *const artifact_product.BackendArtifactPlan,
217 new_selections: []LaunchTuningSelection,
218 ) void {
219 if (self.tuning_selections.len != 0) self.allocator.free(self.tuning_selections);
220 self.tuning_selections = new_selections;
221 var selection_index: usize = 0;
222 for (self.nodes) |*node| {
223 node.tuning = .{};
224 const planned = artifact_plan.kernels.items[node.kernel_index];
225 if (selection_index < self.tuning_selections.len and self.tuning_selections[selection_index].kernel_id == planned.kernel_id) {
226 node.tuning.selections = self.tuning_selections[selection_index .. selection_index + 1];
227 selection_index += 1;
228 }
229 }
230 }
231 };
232
233 const CompiledFragmentState = struct {
234 allocator: std.mem.Allocator,
235 artifact_plan: artifact_product.BackendArtifactPlan,
236 artifact_fingerprint: u64,
237 launch_plan: OwnedLaunchGraphPlan,
238 fingerprint_value: u64,
239
240 fn deinit(self: *CompiledFragmentState) void {
241 self.launch_plan.deinit();
242 self.artifact_plan.deinit();
243 self.* = undefined;
244 }
245 };
246
247 pub const CompiledFragment = opaque {
248 fn stateConst(self: *const CompiledFragment) *const CompiledFragmentState {
249 return @ptrCast(@alignCast(self));
250 }
251
252 fn stateMut(self: *CompiledFragment) *CompiledFragmentState {
253 return @ptrCast(@alignCast(self));
254 }
255
256 pub fn init(
257 allocator: std.mem.Allocator,
258 artifact_module: *artifact_product.ArtifactJob,
259 ) !*CompiledFragment {
260 var artifact_plan = try artifact_module.copyArtifactPlan(allocator);
261 errdefer artifact_plan.deinit();
262
263 var launch_plan = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
264 errdefer launch_plan.deinit();
265
266 const artifact_fingerprint = artifact_module.fingerprint();
267 const product_stamp = fragmentProductStamp(
268 artifact_fingerprint,
269 launchGraphFingerprint(launch_plan.plan()),
270 );
271
272 const state = allocator.create(CompiledFragmentState) catch return error.OutOfMemory;
273 errdefer allocator.destroy(state);
274 state.* = .{
275 .allocator = allocator,
276 .artifact_plan = artifact_plan,
277 .artifact_fingerprint = artifact_fingerprint,
278 .launch_plan = launch_plan,
279 .fingerprint_value = product_stamp.fingerprint,
280 };
281 const fragment: *CompiledFragment = @ptrCast(state);
282 try fragment.verify();
283 return fragment;
284 }
285
286 pub fn initWithLaunchPlan(
287 allocator: std.mem.Allocator,
288 artifact_module: *artifact_product.ArtifactJob,
289 launch_plan: OwnedLaunchGraphPlan,
290 ) !*CompiledFragment {
291 var owned_launch_plan = launch_plan;
292 errdefer owned_launch_plan.deinit();
293
294 var artifact_plan = try artifact_module.copyArtifactPlan(allocator);
295 errdefer artifact_plan.deinit();
296
297 const artifact_fingerprint = artifact_module.fingerprint();
298 const product_stamp = fragmentProductStamp(
299 artifact_fingerprint,
300 launchGraphFingerprint(owned_launch_plan.plan()),
301 );
302
303 const state = allocator.create(CompiledFragmentState) catch return error.OutOfMemory;
304 errdefer allocator.destroy(state);
305 state.* = .{
306 .allocator = allocator,
307 .artifact_plan = artifact_plan,
308 .artifact_fingerprint = artifact_fingerprint,
309 .launch_plan = owned_launch_plan,
310 .fingerprint_value = product_stamp.fingerprint,
311 };
312 const fragment: *CompiledFragment = @ptrCast(state);
313 try fragment.verify();
314 return fragment;
315 }
316
317 pub fn deinit(self: *CompiledFragment) void {
318 const state = self.stateMut();
319 const allocator = state.allocator;
320 state.deinit();
321 allocator.destroy(state);
322 }
323
324 pub fn verify(self: *const CompiledFragment) !void {
325 const state = self.stateConst();
326 try validateLaunchGraph(
327 state.allocator,
328 &state.artifact_plan,
329 state.launch_plan.plan(),
330 false,
331 );
332 }
333
334 pub fn fingerprint(self: *const CompiledFragment) u64 {
335 return self.stateConst().fingerprint_value;
336 }
337
338 pub fn artifactFingerprint(self: *const CompiledFragment) u64 {
339 return self.stateConst().artifact_fingerprint;
340 }
341
342 pub fn productStamp(self: *const CompiledFragment) choir.product.incremental.ProductStamp {
343 return choir.product.incremental.productStamp(product_name, self.fingerprint());
344 }
345
346 pub fn artifactPlan(self: *const CompiledFragment) *const artifact_product.BackendArtifactPlan {
347 return &self.stateConst().artifact_plan;
348 }
349
350 pub fn launchPlan(self: *const CompiledFragment) LaunchGraphPlan {
351 return self.stateConst().launch_plan.plan();
352 }
353
354 pub fn kernelCount(self: *const CompiledFragment) usize {
355 return self.artifactPlan().kernelCount();
356 }
357
358 pub fn createLaunchGraphPlan(
359 self: *const CompiledFragment,
360 allocator: std.mem.Allocator,
361 launch_options: LaunchOptions,
362 ) gpu.BackendError!OwnedLaunchGraphPlan {
363 const state = self.stateConst();
364 var graph = try state.launch_plan.copyWithLaunchOptions(allocator, launch_options);
365 errdefer graph.deinit();
366 const view = graph.plan();
367 try validateLaunchGraph(allocator, self.artifactPlan(), view, launchGraphNeedsDependencyEvents(view));
368 graph.validated = true;
369 return graph;
370 }
371 };
372
373 pub fn compileFragmentFromArtifactJob(
374 allocator: std.mem.Allocator,
375 artifact_module: *artifact_product.ArtifactJob,
376 ) !*CompiledFragment {
377 return try CompiledFragment.init(allocator, artifact_module);
378 }
379
380 pub fn compileFragmentFromArtifactJobWithLaunchPlan(
381 allocator: std.mem.Allocator,
382 artifact_module: *artifact_product.ArtifactJob,
383 launch_plan: OwnedLaunchGraphPlan,
384 ) !*CompiledFragment {
385 return try CompiledFragment.initWithLaunchPlan(allocator, artifact_module, launch_plan);
386 }
387
388 pub fn createDataflowLaunchGraphPlan(
389 allocator: std.mem.Allocator,
390 artifact_plan: *const artifact_product.BackendArtifactPlan,
391 launch_options: LaunchOptions,
392 ) gpu.BackendError!OwnedLaunchGraphPlan {
393 const kernel_count = artifact_plan.kernels.items.len;
394 const nodes = allocator.alloc(LaunchGraphNode, kernel_count) catch return error.OutOfMemory;
395 errdefer allocator.free(nodes);
396
397 const scheduled = allocator.alloc(bool, kernel_count) catch return error.OutOfMemory;
398 defer allocator.free(scheduled);
399 @memset(scheduled, false);
400
401 const kernel_to_node = allocator.alloc(usize, kernel_count) catch return error.OutOfMemory;
402 defer allocator.free(kernel_to_node);
403 @memset(kernel_to_node, std.math.maxInt(usize));
404
405 for (nodes, 0..) |*node, node_index| {
406 const kernel_index = nextReadyKernelIndex(artifact_plan, scheduled) orelse return error.InvalidArtifact;
407 scheduled[kernel_index] = true;
408 kernel_to_node[kernel_index] = node_index;
409 node.* = .{
410 .kernel_index = kernel_index,
411 .stream = launch_options.stream,
412 .wait_events = if (node_index == 0) launch_options.wait_events else &.{},
413 .signal_event = if (node_index + 1 == kernel_count) launch_options.signal_event else null,
414 .tuning = launch_options.tuning,
415 .runtime_scalar_arguments = launch_options.runtime_scalar_arguments,
416 };
417 }
418
419 var dependencies: std.ArrayListUnmanaged(LaunchGraphDependency) = .empty;
420 errdefer dependencies.deinit(allocator);
421
422 for (nodes, 0..) |consumer_node, consumer_node_index| {
423 const consumer = artifact_plan.kernels.items[consumer_node.kernel_index];
424 for (consumer.input_slot_ids) |slot_id| {
425 const producer_index = producerKernelIndexForConsumedSlot(
426 artifact_plan,
427 slot_id,
428 consumer_node.kernel_index,
429 ) orelse continue;
430 if (producer_index == consumer_node.kernel_index) continue;
431 const producer_node_index = kernel_to_node[producer_index];
432 if (producer_node_index == std.math.maxInt(usize)) return error.InvalidArtifact;
433 const dependency = LaunchGraphDependency{
434 .producer_node_index = producer_node_index,
435 .consumer_node_index = consumer_node_index,
436 .slot_id = slot_id,
437 };
438 if (dependencyExists(dependencies.items, dependency.producer_node_index, dependency.consumer_node_index, dependency.slot_id)) continue;
439 dependencies.append(allocator, dependency) catch return error.OutOfMemory;
440 }
441 }
442
443 return .{
444 .allocator = allocator,
445 .nodes = nodes,
446 .dependencies = dependencies.toOwnedSlice(allocator) catch return error.OutOfMemory,
447 };
448 }
449
450 pub fn launchGraphFingerprint(graph: LaunchGraphPlan) u64 {
451 var hasher = choir.product.incremental.FingerprintBuilder{};
452 hasher.updateBytes("accy.exec.launch_graph");
453 hashU64(&hasher, graph.nodes.len);
454 for (graph.nodes) |node| {
455 hashU64(&hasher, node.kernel_index);
456 }
457 hashU64(&hasher, graph.dependencies.len);
458 for (graph.dependencies) |dependency| {
459 hashU64(&hasher, dependency.producer_node_index);
460 hashU64(&hasher, dependency.consumer_node_index);
461 hashU64(&hasher, dependency.slot_id);
462 }
463 hashU64(&hasher, graph.loops.len);
464 for (graph.loops) |loop| {
465 hashU64(&hasher, loop.first_node_index);
466 hashU64(&hasher, loop.node_count);
467 hashU64(&hasher, loop.trip_count);
468 hashU64(&hasher, loop.carries.len);
469 for (loop.carries) |carry| {
470 hashU64(&hasher, carry.initial_slot_id);
471 hashU64(&hasher, carry.input_slot_id);
472 hashU64(&hasher, carry.output_slot_id);
473 hashU64(&hasher, carry.final_slot_id);
474 }
475 }
476 return hasher.finish();
477 }
478
479 pub fn validateLaunchGraph(
480 scratch: std.mem.Allocator,
481 artifact_plan: *const artifact_product.BackendArtifactPlan,
482 graph: LaunchGraphPlan,
483 dependency_events: bool,
484 ) gpu.BackendError!void {
485 if (graph.nodes.len != artifact_plan.kernels.items.len) return error.InvalidArtifact;
486 const seen = scratch.alloc(bool, artifact_plan.kernels.items.len) catch return error.OutOfMemory;
487 defer scratch.free(seen);
488 @memset(seen, false);
489 for (graph.nodes) |node| {
490 if (node.kernel_index >= artifact_plan.kernels.items.len) return error.InvalidArtifact;
491 if (seen[node.kernel_index]) return error.InvalidArtifact;
492 seen[node.kernel_index] = true;
493 }
494 for (graph.dependencies, 0..) |dependency, dependency_index| {
495 if (dependency.producer_node_index >= graph.nodes.len) return error.InvalidArtifact;
496 if (dependency.consumer_node_index >= graph.nodes.len) return error.InvalidArtifact;
497 if (dependency.producer_node_index == dependency.consumer_node_index) return error.InvalidArtifact;
498 if (dependency.producer_node_index >= dependency.consumer_node_index) return error.InvalidArtifact;
499 if (dependencyExists(graph.dependencies[0..dependency_index], dependency.producer_node_index, dependency.consumer_node_index, dependency.slot_id)) {
500 return error.InvalidArtifact;
501 }
502 const producer = graph.nodes[dependency.producer_node_index];
503 const consumer = graph.nodes[dependency.consumer_node_index];
504 if (dependency_events and producer.stream == null) return error.LaunchArgumentMismatch;
505 if (artifact_plan.kernels.items[producer.kernel_index].output_slot_id != dependency.slot_id) return error.InvalidArtifact;
506 if (!kernelConsumesSlot(artifact_plan.kernels.items[consumer.kernel_index], dependency.slot_id)) return error.InvalidArtifact;
507 }
508 try validateLaunchGraphDependencyClosure(artifact_plan, graph);
509 try validateLaunchGraphLoops(scratch, artifact_plan, graph);
510 }
511
512 pub fn launchGraphNeedsDependencyEvents(graph: LaunchGraphPlan) bool {
513 for (graph.dependencies) |dependency| {
514 if (dependency.producer_node_index >= graph.nodes.len) return false;
515 if (dependency.consumer_node_index >= graph.nodes.len) return false;
516 const producer = graph.nodes[dependency.producer_node_index];
517 const consumer = graph.nodes[dependency.consumer_node_index];
518 if (!sameLaunchStream(producer.stream, consumer.stream)) return true;
519 }
520 return false;
521 }
522
523 pub fn incomingDependencyCount(graph: LaunchGraphPlan, node_index: usize) usize {
524 var count: usize = 0;
525 for (graph.dependencies) |dependency| {
526 if (dependency.consumer_node_index == node_index) count += 1;
527 }
528 return count;
529 }
530
531 fn sameLaunchStream(lhs: ?gpu.StreamHandle, rhs: ?gpu.StreamHandle) bool {
532 if (lhs == null and rhs == null) return true;
533 if (lhs == null or rhs == null) return false;
534 return lhs.?.id == rhs.?.id and lhs.?.backend == rhs.?.backend;
535 }
536
537 fn producerKernelIndexForConsumedSlot(
538 artifact_plan: *const artifact_product.BackendArtifactPlan,
539 slot_id: usize,
540 consumer_kernel_index: usize,
541 ) ?usize {
542 var preceding_producer: ?usize = null;
543 for (artifact_plan.kernels.items[0..consumer_kernel_index], 0..) |kernel, index| {
544 if (kernel.output_slot_id == slot_id) preceding_producer = index;
545 }
546 if (preceding_producer) |producer| return producer;
547
548 var unique_producer: ?usize = null;
549 for (artifact_plan.kernels.items, 0..) |kernel, index| {
550 if (kernel.output_slot_id != slot_id) continue;
551 if (unique_producer != null) return null;
552 unique_producer = index;
553 }
554 if (unique_producer) |producer| {
555 if (producer != consumer_kernel_index) return producer;
556 }
557 return null;
558 }
559
560 fn nextReadyKernelIndex(
561 artifact_plan: *const artifact_product.BackendArtifactPlan,
562 scheduled: []const bool,
563 ) ?usize {
564 for (artifact_plan.kernels.items, 0..) |_, kernel_index| {
565 if (scheduled[kernel_index]) continue;
566 if (kernelDependenciesScheduled(artifact_plan, scheduled, kernel_index)) return kernel_index;
567 }
568 return null;
569 }
570
571 fn kernelDependenciesScheduled(
572 artifact_plan: *const artifact_product.BackendArtifactPlan,
573 scheduled: []const bool,
574 consumer_kernel_index: usize,
575 ) bool {
576 const consumer = artifact_plan.kernels.items[consumer_kernel_index];
577 for (consumer.input_slot_ids) |slot_id| {
578 const producer_index = producerKernelIndexForConsumedSlot(
579 artifact_plan,
580 slot_id,
581 consumer_kernel_index,
582 ) orelse continue;
583 if (producer_index == consumer_kernel_index) continue;
584 if (!scheduled[producer_index]) return false;
585 }
586 return true;
587 }
588
589 fn validateLaunchGraphDependencyClosure(
590 artifact_plan: *const artifact_product.BackendArtifactPlan,
591 graph: LaunchGraphPlan,
592 ) gpu.BackendError!void {
593 for (graph.nodes, 0..) |consumer_node, consumer_node_index| {
594 const consumer = artifact_plan.kernels.items[consumer_node.kernel_index];
595 for (consumer.input_slot_ids) |slot_id| {
596 const producer_node_index = producerNodeIndexForConsumedSlot(
597 artifact_plan,
598 graph,
599 slot_id,
600 consumer_node.kernel_index,
601 ) orelse continue;
602 if (producer_node_index == consumer_node_index) continue;
603 if (producer_node_index >= consumer_node_index) return error.InvalidArtifact;
604 if (!dependencyExists(graph.dependencies, producer_node_index, consumer_node_index, slot_id)) {
605 return error.InvalidArtifact;
606 }
607 }
608 }
609 }
610
611 fn validateLaunchGraphLoops(
612 scratch: std.mem.Allocator,
613 artifact_plan: *const artifact_product.BackendArtifactPlan,
614 graph: LaunchGraphPlan,
615 ) gpu.BackendError!void {
616 if (graph.loops.len == 0) return;
617 const loop_node = scratch.alloc(bool, graph.nodes.len) catch return error.OutOfMemory;
618 defer scratch.free(loop_node);
619 @memset(loop_node, false);
620
621 for (graph.loops) |loop| {
622 if (loop.node_count == 0) return error.InvalidArtifact;
623 const last_node_index = std.math.add(usize, loop.first_node_index, loop.node_count) catch return error.InvalidArtifact;
624 if (last_node_index > graph.nodes.len) return error.InvalidArtifact;
625 for (loop_node[loop.first_node_index..last_node_index]) |*seen| {
626 if (seen.*) return error.InvalidArtifact;
627 seen.* = true;
628 }
629 for (loop.carries) |carry| {
630 if (carry.input_slot_id == carry.output_slot_id) return error.InvalidArtifact;
631 if (loop.trip_count != 0 and carry.initial_slot_id == carry.output_slot_id) return error.InvalidArtifact;
632 if (launchGraphLoopCarryFinalSlot(carry, loop.trip_count) != carry.final_slot_id) return error.InvalidArtifact;
633 if (!loopConsumesSlot(artifact_plan, graph, loop, carry.input_slot_id)) return error.InvalidArtifact;
634 if (!loopProducesSlot(artifact_plan, graph, loop, carry.output_slot_id)) return error.InvalidArtifact;
635 }
636 }
637 }
638
639 fn loopConsumesSlot(
640 artifact_plan: *const artifact_product.BackendArtifactPlan,
641 graph: LaunchGraphPlan,
642 loop: LaunchGraphLoop,
643 slot_id: usize,
644 ) bool {
645 const end = loop.first_node_index + loop.node_count;
646 for (graph.nodes[loop.first_node_index..end]) |node| {
647 if (kernelConsumesSlot(artifact_plan.kernels.items[node.kernel_index], slot_id)) return true;
648 }
649 return false;
650 }
651
652 fn loopProducesSlot(
653 artifact_plan: *const artifact_product.BackendArtifactPlan,
654 graph: LaunchGraphPlan,
655 loop: LaunchGraphLoop,
656 slot_id: usize,
657 ) bool {
658 const end = loop.first_node_index + loop.node_count;
659 for (graph.nodes[loop.first_node_index..end]) |node| {
660 if (artifact_plan.kernels.items[node.kernel_index].output_slot_id == slot_id) return true;
661 }
662 return false;
663 }
664
665 fn producerNodeIndexForConsumedSlot(
666 artifact_plan: *const artifact_product.BackendArtifactPlan,
667 graph: LaunchGraphPlan,
668 slot_id: usize,
669 consumer_kernel_index: usize,
670 ) ?usize {
671 const producer_kernel_index = producerKernelIndexForConsumedSlot(
672 artifact_plan,
673 slot_id,
674 consumer_kernel_index,
675 ) orelse return null;
676 for (graph.nodes, 0..) |node, node_index| {
677 if (node.kernel_index == producer_kernel_index) return node_index;
678 }
679 return null;
680 }
681
682 fn dependencyExists(
683 dependencies: []const LaunchGraphDependency,
684 producer_node_index: usize,
685 consumer_node_index: usize,
686 slot_id: usize,
687 ) bool {
688 for (dependencies) |dependency| {
689 if (dependency.producer_node_index != producer_node_index) continue;
690 if (dependency.consumer_node_index != consumer_node_index) continue;
691 if (dependency.slot_id != slot_id) continue;
692 return true;
693 }
694 return false;
695 }
696
697 fn kernelConsumesSlot(kernel: artifact_product.PlannedKernel, slot_id: usize) bool {
698 for (kernel.input_slot_ids) |input_slot_id| {
699 if (input_slot_id == slot_id) return true;
700 }
701 return false;
702 }
703
704 pub fn fragmentProductStamp(
705 artifact_fingerprint: u64,
706 launch_graph_fingerprint: u64,
707 ) choir.product.incremental.ProductStamp {
708 const artifact_stamp = choir.product.incremental.productStamp(
709 artifact_product.product_name,
710 artifact_fingerprint,
711 );
712 return choir.product.incremental.derivedProductStamp(
713 product_name,
714 &.{artifact_stamp},
715 launch_graph_fingerprint,
716 );
717 }
718
719 fn hashU64(hasher: *choir.product.incremental.FingerprintBuilder, value: u64) void {
720 hasher.updateU64(value);
721 }
722
723 const LaunchCandidateMeasurement = tuning_mod.LaunchCandidateMeasurement;
724 const LaunchTuningSelection = tuning_mod.LaunchTuningSelection;
725 const LaunchTuningCache = tuning_mod.LaunchTuningCache;
726 const LaunchTuning = tuning_mod.LaunchTuning;
727 const selectedLaunchMeasurement = tuning_mod.selectedLaunchMeasurement;
728
729 fn appendTestKernel(
730 allocator: std.mem.Allocator,
731 artifact_plan: *artifact_product.BackendArtifactPlan,
732 kernel_id: usize,
733 output_slot_id: usize,
734 input_slot_ids: []const usize,
735 ) !void {
736 const inputs = try allocator.dupe(usize, input_slot_ids);
737 errdefer allocator.free(inputs);
738
739 const entry_name = if (kernel_id == 0) "kernel0" else "kernel1";
740 const compile_entry_name = try allocator.dupe(u8, entry_name);
741 var compile_entry_owned = true;
742 errdefer if (compile_entry_owned) allocator.free(compile_entry_name);
743
744 var artifact = try gpu.KernelArtifact.init(allocator, .{
745 .backend = .cuda,
746 .format = .cuda_ptx,
747 .entry_name = entry_name,
748 .argument_count = 0,
749 });
750 errdefer artifact.deinit();
751
752 try artifact_plan.kernels.append(allocator, .{
753 .compile = .{
754 .source = .tensor,
755 .launch = .generic,
756 .format = .cuda_ptx,
757 .entry_name = compile_entry_name,
758 .argument_count = 0,
759 .required_dtypes = gpu.DTypeSet.init(&.{.f32}),
760 .payload = .none,
761 .payload_byte_count = 0,
762 },
763 .kernel_id = kernel_id,
764 .work_item_id = kernel_id,
765 .output_slot_id = output_slot_id,
766 .input_slot_ids = inputs,
767 .output_layout_fingerprint = 0,
768 .input_layout_fingerprint = 0,
769 .element_count = 1,
770 .op_count = 1,
771 .resources = .{
772 .element_count = 1,
773 .element_size = 4,
774 .op_count = 1,
775 },
776 .artifact = artifact,
777 .launch_resources = .{
778 .format = .cuda_ptx,
779 .element_count = 1,
780 .geometry = .{
781 .grid = .{ 1, 1, 1 },
782 .threadgroup = .{ 1, 1, 1 },
783 },
784 },
785 .element_count_argument = .none,
786 .element_count_argument_value = 0,
787 .runtime_scalar_argument_count = 0,
788 .runtime_scalar_defaults = &.{},
789 .static_arguments = &.{},
790 });
791 compile_entry_owned = false;
792 }
793
794 test "compiled fragment copies artifact job plan" {
795 const allocator = std.testing.allocator;
796
797 var artifact_plan = artifact_product.BackendArtifactPlan.init(allocator, .{
798 .backend_kind = .cuda,
799 .artifact_format = .cuda_ptx,
800 .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
801 });
802 var plan_owned = true;
803 errdefer if (plan_owned) artifact_plan.deinit();
804
805 const artifact_module = try artifact_product.ArtifactJob.init(
806 allocator,
807 artifact_plan,
808 );
809 plan_owned = false;
810 var artifact_owned = true;
811 errdefer if (artifact_owned) artifact_module.deinit();
812
813 const artifact_fingerprint = artifact_module.fingerprint();
814 var first = try CompiledFragment.init(allocator, artifact_module);
815 errdefer first.deinit();
816 var second = try CompiledFragment.init(allocator, artifact_module);
817 errdefer second.deinit();
818
819 artifact_module.deinit();
820 artifact_owned = false;
821 defer second.deinit();
822 defer first.deinit();
823
824 try std.testing.expectEqualStrings(product_name, "accy.exec");
825 try first.verify();
826 try second.verify();
827 const expected_stamp = choir.product.incremental.derivedProductStamp(
828 product_name,
829 &.{choir.product.incremental.productStamp(artifact_product.product_name, artifact_fingerprint)},
830 launchGraphFingerprint(first.launchPlan()),
831 );
832 try std.testing.expectEqualStrings(product_name, expected_stamp.name);
833 try std.testing.expectEqual(expected_stamp.fingerprint, first.fingerprint());
834 try std.testing.expectEqual(first.fingerprint(), second.fingerprint());
835 try std.testing.expectEqualStrings(product_name, first.productStamp().name);
836 try std.testing.expectEqual(first.fingerprint(), first.productStamp().fingerprint);
837 try std.testing.expectEqual(artifact_fingerprint, first.artifactFingerprint());
838 try std.testing.expectEqual(@as(usize, 0), first.kernelCount());
839 try std.testing.expectEqual(@as(usize, 0), first.launchPlan().nodes.len);
840 }
841
842 test "failed compiled fragment init leaves artifact ownership with caller" {
843 const allocator = std.testing.allocator;
844
845 var artifact_plan = artifact_product.BackendArtifactPlan.init(allocator, .{
846 .backend_kind = .cuda,
847 .artifact_format = .cuda_ptx,
848 .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
849 });
850 var plan_owned = true;
851 errdefer if (plan_owned) artifact_plan.deinit();
852
853 try appendTestKernel(allocator, &artifact_plan, 0, 2, &.{3});
854 try appendTestKernel(allocator, &artifact_plan, 1, 3, &.{2});
855
856 const artifact_module = try artifact_product.ArtifactJob.init(
857 allocator,
858 artifact_plan,
859 );
860 plan_owned = false;
861 defer artifact_module.deinit();
862
863 try std.testing.expectError(
864 error.InvalidArtifact,
865 CompiledFragment.init(allocator, artifact_module),
866 );
867 }
868
869 test "executable dataflow launch graph records slot dependencies" {
870 const allocator = std.testing.allocator;
871
872 var artifact_plan = artifact_product.BackendArtifactPlan.init(allocator, .{
873 .backend_kind = .cuda,
874 .artifact_format = .cuda_ptx,
875 .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
876 });
877 defer artifact_plan.deinit();
878 try appendTestKernel(allocator, &artifact_plan, 0, 2, &.{0});
879 try appendTestKernel(allocator, &artifact_plan, 1, 3, &.{2});
880
881 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
882 defer graph.deinit();
883
884 try std.testing.expectEqual(@as(usize, 2), graph.nodes.len);
885 try std.testing.expectEqual(@as(usize, 0), graph.nodes[0].kernel_index);
886 try std.testing.expectEqual(@as(usize, 1), graph.nodes[1].kernel_index);
887 try std.testing.expectEqual(@as(usize, 1), graph.dependencies.len);
888 try std.testing.expectEqual(@as(usize, 0), graph.dependencies[0].producer_node_index);
889 try std.testing.expectEqual(@as(usize, 1), graph.dependencies[0].consumer_node_index);
890 try std.testing.expectEqual(@as(usize, 2), graph.dependencies[0].slot_id);
891 try validateLaunchGraph(allocator, &artifact_plan, graph.plan(), false);
892 }
893
894 test "executable dataflow launch graph links repeated in-place slot updates" {
895 const allocator = std.testing.allocator;
896
897 var artifact_plan = artifact_product.BackendArtifactPlan.init(allocator, .{
898 .backend_kind = .cuda,
899 .artifact_format = .cuda_ptx,
900 .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
901 });
902 defer artifact_plan.deinit();
903 try appendTestKernel(allocator, &artifact_plan, 0, 0, &.{0});
904 try appendTestKernel(allocator, &artifact_plan, 1, 0, &.{0});
905 try appendTestKernel(allocator, &artifact_plan, 2, 1, &.{0});
906
907 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
908 defer graph.deinit();
909
910 try std.testing.expectEqual(@as(usize, 3), graph.nodes.len);
911 try std.testing.expectEqual(@as(usize, 0), graph.nodes[0].kernel_index);
912 try std.testing.expectEqual(@as(usize, 1), graph.nodes[1].kernel_index);
913 try std.testing.expectEqual(@as(usize, 2), graph.nodes[2].kernel_index);
914 try std.testing.expectEqual(@as(usize, 2), graph.dependencies.len);
915 try std.testing.expectEqual(@as(usize, 0), graph.dependencies[0].producer_node_index);
916 try std.testing.expectEqual(@as(usize, 1), graph.dependencies[0].consumer_node_index);
917 try std.testing.expectEqual(@as(usize, 0), graph.dependencies[0].slot_id);
918 try std.testing.expectEqual(@as(usize, 1), graph.dependencies[1].producer_node_index);
919 try std.testing.expectEqual(@as(usize, 2), graph.dependencies[1].consumer_node_index);
920 try std.testing.expectEqual(@as(usize, 0), graph.dependencies[1].slot_id);
921 try validateLaunchGraph(allocator, &artifact_plan, graph.plan(), false);
922 }
923
924 test "executable dataflow launch graph orders producers before consumers" {
925 const allocator = std.testing.allocator;
926
927 var artifact_plan = artifact_product.BackendArtifactPlan.init(allocator, .{
928 .backend_kind = .cuda,
929 .artifact_format = .cuda_ptx,
930 .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
931 });
932 defer artifact_plan.deinit();
933 try appendTestKernel(allocator, &artifact_plan, 0, 3, &.{2});
934 try appendTestKernel(allocator, &artifact_plan, 1, 2, &.{0});
935
936 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
937 defer graph.deinit();
938
939 try std.testing.expectEqual(@as(usize, 2), graph.nodes.len);
940 try std.testing.expectEqual(@as(usize, 1), graph.nodes[0].kernel_index);
941 try std.testing.expectEqual(@as(usize, 0), graph.nodes[1].kernel_index);
942 try std.testing.expectEqual(@as(usize, 1), graph.dependencies.len);
943 try std.testing.expectEqual(@as(usize, 0), graph.dependencies[0].producer_node_index);
944 try std.testing.expectEqual(@as(usize, 1), graph.dependencies[0].consumer_node_index);
945 try std.testing.expectEqual(@as(usize, 2), graph.dependencies[0].slot_id);
946 try validateLaunchGraph(allocator, &artifact_plan, graph.plan(), false);
947 }
948
949 test "executable launch graph fingerprint includes schedule order" {
950 const first_nodes = [_]LaunchGraphNode{
951 .{ .kernel_index = 0 },
952 .{ .kernel_index = 1 },
953 };
954 const second_nodes = [_]LaunchGraphNode{
955 .{ .kernel_index = 1 },
956 .{ .kernel_index = 0 },
957 };
958 const dependencies = [_]LaunchGraphDependency{.{
959 .producer_node_index = 0,
960 .consumer_node_index = 1,
961 .slot_id = 2,
962 }};
963
964 try std.testing.expect(launchGraphFingerprint(.{
965 .nodes = &first_nodes,
966 .dependencies = &dependencies,
967 }) != launchGraphFingerprint(.{
968 .nodes = &second_nodes,
969 .dependencies = &dependencies,
970 }));
971 }
972
973 test "executable launch graph fingerprint includes loop structure" {
974 const nodes = [_]LaunchGraphNode{
975 .{ .kernel_index = 0 },
976 .{ .kernel_index = 1 },
977 };
978 const carry = [_]LaunchGraphLoopCarry{.{
979 .initial_slot_id = 0,
980 .input_slot_id = 0,
981 .output_slot_id = 2,
982 .final_slot_id = 0,
983 }};
984 const first_loops = [_]LaunchGraphLoop{.{
985 .first_node_index = 0,
986 .node_count = 2,
987 .trip_count = 4,
988 .carries = &carry,
989 }};
990 const second_loops = [_]LaunchGraphLoop{.{
991 .first_node_index = 0,
992 .node_count = 2,
993 .trip_count = 5,
994 .carries = &carry,
995 }};
996
997 try std.testing.expect(launchGraphFingerprint(.{
998 .nodes = &nodes,
999 .loops = &first_loops,
1000 }) != launchGraphFingerprint(.{
1001 .nodes = &nodes,
1002 .loops = &second_loops,
1003 }));
1004 }
1005
1006 test "executable launch graph validates loop carry body slots" {
1007 const allocator = std.testing.allocator;
1008
1009 var artifact_plan = artifact_product.BackendArtifactPlan.init(allocator, .{
1010 .backend_kind = .cuda,
1011 .artifact_format = .cuda_ptx,
1012 .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
1013 });
1014 defer artifact_plan.deinit();
1015 try appendTestKernel(allocator, &artifact_plan, 0, 1, &.{0});
1016 try appendTestKernel(allocator, &artifact_plan, 1, 2, &.{1});
1017
1018 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
1019 defer graph.deinit();
1020
1021 const carry = [_]LaunchGraphLoopCarry{.{
1022 .initial_slot_id = 0,
1023 .input_slot_id = 0,
1024 .output_slot_id = 2,
1025 .final_slot_id = 0,
1026 }};
1027 const loops = [_]LaunchGraphLoop{.{
1028 .first_node_index = 0,
1029 .node_count = 2,
1030 .trip_count = 4,
1031 .carries = &carry,
1032 }};
1033
1034 try validateLaunchGraph(allocator, &artifact_plan, .{
1035 .nodes = graph.nodes,
1036 .dependencies = graph.dependencies,
1037 .loops = &loops,
1038 }, false);
1039 }
1040
1041 test "executable launch graph rejects invalid loop ranges and carries" {
1042 const allocator = std.testing.allocator;
1043
1044 var artifact_plan = artifact_product.BackendArtifactPlan.init(allocator, .{
1045 .backend_kind = .cuda,
1046 .artifact_format = .cuda_ptx,
1047 .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
1048 });
1049 defer artifact_plan.deinit();
1050 try appendTestKernel(allocator, &artifact_plan, 0, 1, &.{0});
1051 try appendTestKernel(allocator, &artifact_plan, 1, 2, &.{1});
1052
1053 var graph = try createDataflowLaunchGraphPlan(allocator, &artifact_plan, .{});
1054 defer graph.deinit();
1055
1056 const valid_carry = [_]LaunchGraphLoopCarry{.{
1057 .initial_slot_id = 0,
1058 .input_slot_id = 0,
1059 .output_slot_id = 2,
1060 .final_slot_id = 0,
1061 }};
1062 const missing_input = [_]LaunchGraphLoopCarry{.{
1063 .initial_slot_id = 0,
1064 .input_slot_id = 99,
1065 .output_slot_id = 2,
1066 .final_slot_id = 0,
1067 }};
1068 const missing_output = [_]LaunchGraphLoopCarry{.{
1069 .initial_slot_id = 0,
1070 .input_slot_id = 0,
1071 .output_slot_id = 99,
1072 .final_slot_id = 0,
1073 }};
1074 const bad_final = [_]LaunchGraphLoopCarry{.{
1075 .initial_slot_id = 0,
1076 .input_slot_id = 0,
1077 .output_slot_id = 2,
1078 .final_slot_id = 3,
1079 }};
1080
1081 const empty_loop = [_]LaunchGraphLoop{.{
1082 .first_node_index = 0,
1083 .node_count = 0,
1084 .trip_count = 4,
1085 .carries = &valid_carry,
1086 }};
1087 try std.testing.expectError(error.InvalidArtifact, validateLaunchGraph(allocator, &artifact_plan, .{
1088 .nodes = graph.nodes,
1089 .dependencies = graph.dependencies,
1090 .loops = &empty_loop,
1091 }, false));
1092
1093 const out_of_range = [_]LaunchGraphLoop{.{
1094 .first_node_index = 1,
1095 .node_count = 2,
1096 .trip_count = 4,
1097 .carries = &valid_carry,
1098 }};
1099 try std.testing.expectError(error.InvalidArtifact, validateLaunchGraph(allocator, &artifact_plan, .{
1100 .nodes = graph.nodes,
1101 .dependencies = graph.dependencies,
1102 .loops = &out_of_range,
1103 }, false));
1104
1105 const bad_input = [_]LaunchGraphLoop{.{
1106 .first_node_index = 0,
1107 .node_count = 2,
1108 .trip_count = 4,
1109 .carries = &missing_input,
1110 }};
1111 try std.testing.expectError(error.InvalidArtifact, validateLaunchGraph(allocator, &artifact_plan, .{
1112 .nodes = graph.nodes,
1113 .dependencies = graph.dependencies,
1114 .loops = &bad_input,
1115 }, false));
1116
1117 const bad_output = [_]LaunchGraphLoop{.{
1118 .first_node_index = 0,
1119 .node_count = 2,
1120 .trip_count = 4,
1121 .carries = &missing_output,
1122 }};
1123 try std.testing.expectError(error.InvalidArtifact, validateLaunchGraph(allocator, &artifact_plan, .{
1124 .nodes = graph.nodes,
1125 .dependencies = graph.dependencies,
1126 .loops = &bad_output,
1127 }, false));
1128
1129 const invalid_final = [_]LaunchGraphLoop{.{
1130 .first_node_index = 0,
1131 .node_count = 2,
1132 .trip_count = 4,
1133 .carries = &bad_final,
1134 }};
1135 try std.testing.expectError(error.InvalidArtifact, validateLaunchGraph(allocator, &artifact_plan, .{
1136 .nodes = graph.nodes,
1137 .dependencies = graph.dependencies,
1138 .loops = &invalid_final,
1139 }, false));
1140 }
1141
1142 test "executable launch graph rejects missing slot dependencies" {
1143 const allocator = std.testing.allocator;
1144
1145 var artifact_plan = artifact_product.BackendArtifactPlan.init(allocator, .{
1146 .backend_kind = .cuda,
1147 .artifact_format = .cuda_ptx,
1148 .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
1149 });
1150 defer artifact_plan.deinit();
1151 try appendTestKernel(allocator, &artifact_plan, 0, 2, &.{0});
1152 try appendTestKernel(allocator, &artifact_plan, 1, 3, &.{2});
1153
1154 const nodes = [_]LaunchGraphNode{
1155 .{ .kernel_index = 0 },
1156 .{ .kernel_index = 1 },
1157 };
1158
1159 try std.testing.expectError(
1160 error.InvalidArtifact,
1161 validateLaunchGraph(allocator, &artifact_plan, .{ .nodes = &nodes }, false),
1162 );
1163 }
1164
1165 test "executable launch graph rejects duplicate slot dependencies" {
1166 const allocator = std.testing.allocator;
1167
1168 var artifact_plan = artifact_product.BackendArtifactPlan.init(allocator, .{
1169 .backend_kind = .cuda,
1170 .artifact_format = .cuda_ptx,
1171 .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
1172 });
1173 defer artifact_plan.deinit();
1174 try appendTestKernel(allocator, &artifact_plan, 0, 2, &.{0});
1175 try appendTestKernel(allocator, &artifact_plan, 1, 3, &.{2});
1176
1177 const nodes = [_]LaunchGraphNode{
1178 .{ .kernel_index = 0 },
1179 .{ .kernel_index = 1 },
1180 };
1181 const dependencies = [_]LaunchGraphDependency{
1182 .{
1183 .producer_node_index = 0,
1184 .consumer_node_index = 1,
1185 .slot_id = 2,
1186 },
1187 .{
1188 .producer_node_index = 0,
1189 .consumer_node_index = 1,
1190 .slot_id = 2,
1191 },
1192 };
1193
1194 try std.testing.expectError(
1195 error.InvalidArtifact,
1196 validateLaunchGraph(
1197 allocator,
1198 &artifact_plan,
1199 .{
1200 .nodes = &nodes,
1201 .dependencies = &dependencies,
1202 },
1203 false,
1204 ),
1205 );
1206 }