lib/accy/src/executable/binding.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gpu = @import("gpu");
3 const accy_root = @import("../root.zig");
4 const artifact_product = @import("../artifact/root.zig");
5 const plan_mod = @import("plan.zig");
6
7 const BackendArtifactPlan = artifact_product.BackendArtifactPlan;
8 const BackendMemoryPlan = artifact_product.plan.BackendMemoryPlan;
9 const PlannedSlot = artifact_product.PlannedSlot;
10
11 pub const SlotBinding = struct {
12 slot_id: usize,
13 binding: gpu.BufferBinding,
14 };
15
16 pub const ElementCountBufferBinding = struct {
17 kernel_id: usize,
18 binding: gpu.BufferBinding,
19 };
20
21 const BackendPlanBuffer = struct {
22 allocation_id: usize,
23 handle: gpu.BufferHandle,
24 };
25
26 const OutputCopy = struct {
27 output_index: usize,
28 slot_id: usize,
29 };
30
31 const PreparedLaunchBindingsState = struct {
32 allocator: std.mem.Allocator,
33 handle: gpu.BackendHandle,
34 live_buffers: std.ArrayList(gpu.BufferHandle) = .empty,
35 slot_bindings: std.ArrayList(SlotBinding) = .empty,
36 plan_buffers: std.ArrayList(BackendPlanBuffer) = .empty,
37 count_bindings: std.ArrayList(ElementCountBufferBinding) = .empty,
38 output_copies: std.ArrayList(OutputCopy) = .empty,
39 borrowed_count: usize = 0,
40
41 fn create(
42 allocator: std.mem.Allocator,
43 handle: gpu.BackendHandle,
44 ) !*PreparedLaunchBindingsState {
45 const state = try allocator.create(PreparedLaunchBindingsState);
46 state.* = .{ .allocator = allocator, .handle = handle };
47 return state;
48 }
49
50 fn destroy(self: *PreparedLaunchBindingsState) void {
51 const allocator = self.allocator;
52 std.debug.assert(self.borrowed_count <= self.live_buffers.items.len);
53 for (self.live_buffers.items) |buffer| self.handle.destroyObject(buffer.id);
54 self.output_copies.deinit(allocator);
55 self.count_bindings.deinit(allocator);
56 self.plan_buffers.deinit(allocator);
57 self.slot_bindings.deinit(allocator);
58 self.live_buffers.deinit(allocator);
59 self.* = undefined;
60 allocator.destroy(self);
61 }
62 };
63
64 pub const PreparedLaunchBindings = opaque {
65 fn stateConst(self: *const PreparedLaunchBindings) *const PreparedLaunchBindingsState {
66 return @ptrCast(@alignCast(self));
67 }
68
69 fn stateMut(self: *PreparedLaunchBindings) *PreparedLaunchBindingsState {
70 return @ptrCast(@alignCast(self));
71 }
72
73 pub fn deinit(self: *PreparedLaunchBindings) void {
74 self.stateMut().destroy();
75 }
76 };
77
78 pub fn liveBufferCount(bindings: *const PreparedLaunchBindings) usize {
79 const state = bindings.stateConst();
80 return state.live_buffers.items.len;
81 }
82
83 pub fn slotBindings(bindings: *const PreparedLaunchBindings) []const SlotBinding {
84 const state = bindings.stateConst();
85 return state.slot_bindings.items;
86 }
87
88 pub fn elementCountBindings(bindings: *const PreparedLaunchBindings) []const ElementCountBufferBinding {
89 const state = bindings.stateConst();
90 return state.count_bindings.items;
91 }
92
93 pub fn elementCountBindingForKernel(
94 bindings: *const PreparedLaunchBindings,
95 kernel_id: usize,
96 ) ?gpu.BufferBinding {
97 const state = bindings.stateConst();
98 for (state.count_bindings.items) |binding| {
99 if (binding.kernel_id == kernel_id) return binding.binding;
100 }
101 return null;
102 }
103
104 /// Returns how many buffers `prepareBorrowed` imported from caller memory, so a
105 /// caller or a test can see how many buffers of one run use that memory in
106 /// place.
107 pub fn borrowedBufferCount(bindings: *const PreparedLaunchBindings) usize {
108 const state = bindings.stateConst();
109 std.debug.assert(state.borrowed_count <= state.live_buffers.items.len);
110 return state.borrowed_count;
111 }
112
113 pub fn prepare(
114 allocator: std.mem.Allocator,
115 handle: gpu.BackendHandle,
116 artifact_plan: *const BackendArtifactPlan,
117 inputs: []const []const u8,
118 ) !*PreparedLaunchBindings {
119 if (artifact_plan.input_slot_ids.len != inputs.len) return error.InvalidArtifact;
120 const bindings = try PreparedLaunchBindingsState.create(allocator, handle);
121 errdefer bindings.destroy();
122 const min_buffer_alignment = try backendMinBufferAlignment(handle);
123
124 for (artifact_plan.input_slot_ids, inputs) |slot_id, input| {
125 const slot = artifact_plan.slotById(slot_id) orelse return error.InvalidArtifact;
126 if (slot.byte_size == null or input.len != slot.byte_size.?) return error.InvalidArtifact;
127 try bindCopiedInput(bindings, slot.*, input, min_buffer_alignment);
128 }
129
130 var memory_plan = try artifact_product.createBackendMemoryPlan(allocator, artifact_plan);
131 defer memory_plan.deinit();
132 try bindPlannedSlots(bindings, artifact_plan, &memory_plan, min_buffer_alignment);
133 try bindElementCounts(bindings, artifact_plan, min_buffer_alignment);
134 return @ptrCast(bindings);
135 }
136
137 /// Binds the caller's `inputs` and `outputs` to planned buffers of one run, and
138 /// imports each slice in place when the plans show that no kernel will write
139 /// through it by surprise, so the caller runs a compiled program directly on
140 /// its own input and output slices, avoiding a copy when the plans allow it. An
141 /// input is imported read-only when no kernel output and no loop carry writes
142 /// its slot, and the call copies the input into a new buffer otherwise. An
143 /// output is copied back later by `completeBorrowed` when its slot appears
144 /// earlier in the output list, has no planned allocation, is an input, is a
145 /// constant, or is a loop carry. Every other output is imported as the buffer
146 /// of its planned allocation, and two outputs that share one allocation give
147 /// `error.InvalidArtifact`. Input and output counts that differ from the plan
148 /// give `error.InvalidArtifact`, and an output that overlaps an input or an
149 /// earlier output gives `error.InvalidBuffer`. Each slice must be exactly its
150 /// slot's size, and each imported slice must meet the slot's alignment, else
151 /// the call returns `error.InvalidBuffer`. Imported slices are used in place by
152 /// the backend, so the caller keeps every slice alive and unmoved until the
153 /// returned bindings are released with `deinit`.
154 pub fn prepareBorrowed(
155 allocator: std.mem.Allocator,
156 handle: gpu.BackendHandle,
157 artifact_plan: *const BackendArtifactPlan,
158 launch_plan: plan_mod.LaunchGraphPlan,
159 inputs: []const []const u8,
160 outputs: []const []u8,
161 ) !*PreparedLaunchBindings {
162 if (artifact_plan.input_slot_ids.len != inputs.len) return error.InvalidArtifact;
163 if (artifact_plan.output_slot_ids.len != outputs.len) return error.InvalidArtifact;
164 try expectDisjointOutputs(inputs, outputs);
165 const bindings = try PreparedLaunchBindingsState.create(allocator, handle);
166 errdefer bindings.destroy();
167 const min_buffer_alignment = try backendMinBufferAlignment(handle);
168
169 for (artifact_plan.input_slot_ids, inputs) |slot_id, input| {
170 const slot = artifact_plan.slotById(slot_id) orelse return error.InvalidArtifact;
171 if (try slotByteSize(slot.*) != input.len) return error.InvalidBuffer;
172 if (slotIsWritten(artifact_plan, launch_plan, slot_id)) {
173 try bindCopiedInput(bindings, slot.*, input, min_buffer_alignment);
174 continue;
175 }
176 const buffer = try importSlotBuffer(
177 bindings,
178 slot.*,
179 @constCast(input),
180 min_buffer_alignment,
181 );
182 try appendSlotBindingWithAccess(
183 allocator,
184 &bindings.slot_bindings,
185 slot_id,
186 buffer,
187 .read_only,
188 );
189 }
190
191 var memory_plan = try artifact_product.createBackendMemoryPlan(allocator, artifact_plan);
192 defer memory_plan.deinit();
193 for (outputs, 0..) |output, index| {
194 try bindBorrowedOutput(
195 bindings,
196 artifact_plan,
197 launch_plan,
198 &memory_plan,
199 index,
200 output,
201 min_buffer_alignment,
202 );
203 }
204 try bindPlannedSlots(bindings, artifact_plan, &memory_plan, min_buffer_alignment);
205 try bindElementCounts(bindings, artifact_plan, min_buffer_alignment);
206 return @ptrCast(bindings);
207 }
208
209 /// Reads each output that `prepareBorrowed` set aside for copying from its
210 /// device buffer into the caller's slice, so the caller fills outputs that
211 /// could not be written in place. The caller calls this function only after the
212 /// launch has completed, as the session does. An output index beyond `outputs`
213 /// gives `error.InvalidBuffer`.
214 pub fn completeBorrowed(
215 bindings: *const PreparedLaunchBindings,
216 outputs: []const []u8,
217 ) gpu.BackendError!void {
218 const state = bindings.stateConst();
219 for (state.output_copies.items) |copy| {
220 if (copy.output_index >= outputs.len) return error.InvalidBuffer;
221 const binding = try bindingForSlot(state.slot_bindings.items, copy.slot_id, .read_only);
222 try state.handle.readBuffer(.{
223 .handle = binding.handle,
224 .bytes = outputs[copy.output_index],
225 });
226 }
227 }
228
229 fn bindCopiedInput(
230 state: *PreparedLaunchBindingsState,
231 slot: PlannedSlot,
232 input: []const u8,
233 min_buffer_alignment: u32,
234 ) !void {
235 const buffer = try allocateBackendSlotBuffer(
236 state.allocator,
237 state.handle,
238 &state.live_buffers,
239 slot,
240 min_buffer_alignment,
241 );
242 try state.handle.writeBuffer(.{ .handle = buffer, .bytes = input });
243 try appendSlotBinding(state.allocator, &state.slot_bindings, slot.slot_id, buffer);
244 }
245
246 fn bindBorrowedOutput(
247 state: *PreparedLaunchBindingsState,
248 artifact_plan: *const BackendArtifactPlan,
249 launch_plan: plan_mod.LaunchGraphPlan,
250 memory_plan: *const BackendMemoryPlan,
251 index: usize,
252 output: []u8,
253 min_buffer_alignment: u32,
254 ) !void {
255 const slot_id = artifact_plan.output_slot_ids[index];
256 const slot = artifact_plan.slotById(slot_id) orelse return error.InvalidArtifact;
257 if (try slotByteSize(slot.*) != output.len) return error.InvalidBuffer;
258 const earlier_outputs = artifact_plan.output_slot_ids[0..index];
259 const repeated = std.mem.indexOfScalar(usize, earlier_outputs, slot_id) != null;
260 const assignment = memory_plan.assignmentForSlot(slot_id);
261 if (repeated or assignment == null or slot.role.input or slot.role.constant or
262 slotIsCarried(launch_plan, slot_id))
263 {
264 try state.output_copies.append(state.allocator, .{
265 .output_index = index,
266 .slot_id = slot_id,
267 });
268 return;
269 }
270 const allocation_id = assignment.?.allocation_id;
271 const existing = planBufferHandleFor(state.plan_buffers.items, allocation_id);
272 if (existing != null) return error.InvalidArtifact;
273 const buffer = try importSlotBuffer(state, slot.*, output, min_buffer_alignment);
274 try state.plan_buffers.append(state.allocator, .{
275 .allocation_id = allocation_id,
276 .handle = buffer,
277 });
278 }
279
280 fn importSlotBuffer(
281 state: *PreparedLaunchBindingsState,
282 slot: PlannedSlot,
283 bytes: []u8,
284 min_buffer_alignment: u32,
285 ) !gpu.BufferHandle {
286 const buffer = try state.handle.importBuffer(.{
287 .bytes = bytes,
288 .alignment = try backendAllocationAlignment(slot.alignment, min_buffer_alignment),
289 .dtype = slot.dtype,
290 .element_count = slot.element_count,
291 });
292 errdefer state.handle.destroyObject(buffer.id);
293 try state.live_buffers.append(state.allocator, buffer);
294 state.borrowed_count += 1;
295 return buffer;
296 }
297
298 fn slotByteSize(slot: PlannedSlot) !usize {
299 const byte_size = slot.byte_size orelse return error.UnsupportedOperation;
300 return std.math.cast(usize, byte_size) orelse error.InvalidArtifact;
301 }
302
303 fn slotIsWritten(
304 artifact_plan: *const BackendArtifactPlan,
305 launch_plan: plan_mod.LaunchGraphPlan,
306 slot_id: usize,
307 ) bool {
308 for (artifact_plan.kernels.items) |kernel| {
309 if (kernel.output_slot_id == slot_id) return true;
310 }
311 return slotIsCarried(launch_plan, slot_id);
312 }
313
314 fn slotIsCarried(launch_plan: plan_mod.LaunchGraphPlan, slot_id: usize) bool {
315 for (launch_plan.loops) |loop| {
316 for (loop.carries) |carry| {
317 if (carry.initial_slot_id == slot_id or carry.input_slot_id == slot_id) return true;
318 if (carry.output_slot_id == slot_id or carry.final_slot_id == slot_id) return true;
319 }
320 }
321 return false;
322 }
323
324 fn expectDisjointOutputs(
325 inputs: []const []const u8,
326 outputs: []const []u8,
327 ) gpu.BackendError!void {
328 for (outputs, 0..) |output, index| {
329 for (inputs) |input| {
330 if (bytesOverlap(output, input)) return error.InvalidBuffer;
331 }
332 for (outputs[0..index]) |earlier| {
333 if (bytesOverlap(output, earlier)) return error.InvalidBuffer;
334 }
335 }
336 }
337
338 fn bytesOverlap(a: []const u8, b: []const u8) bool {
339 if (a.len == 0 or b.len == 0) return false;
340 const a_start = @intFromPtr(a.ptr);
341 const b_start = @intFromPtr(b.ptr);
342 return a_start < b_start + b.len and b_start < a_start + a.len;
343 }
344
345 fn bindPlannedSlots(
346 state: *PreparedLaunchBindingsState,
347 artifact_plan: *const BackendArtifactPlan,
348 memory_plan: *const BackendMemoryPlan,
349 min_buffer_alignment: u32,
350 ) !void {
351 const allocator = state.allocator;
352 for (artifact_plan.slots) |slot| {
353 if (slot.role.input) continue;
354 if (slot.role.constant) {
355 const used = slot.role.output or plannedSlotHasKernelUse(artifact_plan, slot.slot_id);
356 if (!used) continue;
357 try bindConstantSlot(state, slot, min_buffer_alignment);
358 continue;
359 }
360 if (!slot.hasStaticSize()) return error.UnsupportedOperation;
361 const assignment = memory_plan.assignmentForSlot(slot.slot_id) orelse continue;
362 const planned = planBufferHandleFor(state.plan_buffers.items, assignment.allocation_id);
363 const slot_handle = planned orelse blk: {
364 const allocated = try allocateBackendSlotBuffer(
365 allocator,
366 state.handle,
367 &state.live_buffers,
368 slot,
369 min_buffer_alignment,
370 );
371 try state.plan_buffers.append(allocator, .{
372 .allocation_id = assignment.allocation_id,
373 .handle = allocated,
374 });
375 break :blk allocated;
376 };
377 try appendSlotBinding(allocator, &state.slot_bindings, slot.slot_id, slot_handle);
378 }
379 }
380
381 fn bindConstantSlot(
382 state: *PreparedLaunchBindingsState,
383 slot: PlannedSlot,
384 min_buffer_alignment: u32,
385 ) !void {
386 const bytes = slot.constantBytes() orelse return error.InvalidArtifact;
387 if (bytes.len != try slotByteSize(slot)) return error.InvalidArtifact;
388 const buffer = try allocateBackendSlotBuffer(
389 state.allocator,
390 state.handle,
391 &state.live_buffers,
392 slot,
393 min_buffer_alignment,
394 );
395 try state.handle.writeBuffer(.{ .handle = buffer, .bytes = bytes });
396 try appendSlotBindingWithAccess(
397 state.allocator,
398 &state.slot_bindings,
399 slot.slot_id,
400 buffer,
401 .read_only,
402 );
403 }
404
405 fn bindElementCounts(
406 state: *PreparedLaunchBindingsState,
407 artifact_plan: *const BackendArtifactPlan,
408 min_buffer_alignment: u32,
409 ) !void {
410 for (artifact_plan.kernels.items) |kernel| {
411 if (kernel.element_count_argument != .device_buffer_u32) continue;
412 const count_handle = try allocateBackendElementCountBuffer(
413 state.allocator,
414 state.handle,
415 &state.live_buffers,
416 min_buffer_alignment,
417 );
418 if (kernel.element_count > std.math.maxInt(u32)) return error.LaunchArgumentMismatch;
419 var count_value: u32 = @intCast(kernel.element_count);
420 try state.handle.writeBuffer(.{
421 .handle = count_handle,
422 .bytes = std.mem.asBytes(&count_value),
423 });
424 try state.count_bindings.append(state.allocator, .{
425 .kernel_id = kernel.kernel_id,
426 .binding = executableBackendBinding(count_handle, .read_write),
427 });
428 }
429 }
430
431 fn allocateBackendSlotBuffer(
432 allocator: std.mem.Allocator,
433 handle: gpu.BackendHandle,
434 list: *std.ArrayList(gpu.BufferHandle),
435 slot: artifact_product.PlannedSlot,
436 min_buffer_alignment: u32,
437 ) !gpu.BufferHandle {
438 const byte_size_u64 = slot.byte_size orelse return error.UnsupportedOperation;
439 const byte_size = std.math.cast(usize, byte_size_u64) orelse return error.InvalidArtifact;
440 const buffer = try handle.allocateBuffer(.{
441 .byte_size = byte_size,
442 .alignment = try backendAllocationAlignment(slot.alignment, min_buffer_alignment),
443 .dtype = slot.dtype,
444 .element_count = slot.element_count,
445 });
446 errdefer handle.destroyObject(buffer.id);
447 try list.append(allocator, buffer);
448 return buffer;
449 }
450
451 fn allocateBackendElementCountBuffer(
452 allocator: std.mem.Allocator,
453 handle: gpu.BackendHandle,
454 list: *std.ArrayList(gpu.BufferHandle),
455 min_buffer_alignment: u32,
456 ) !gpu.BufferHandle {
457 const buffer = try handle.allocateBuffer(.{
458 .byte_size = @sizeOf(u32),
459 .alignment = try backendAllocationAlignment(64, min_buffer_alignment),
460 .dtype = .i32,
461 .element_count = 1,
462 });
463 errdefer handle.destroyObject(buffer.id);
464 try list.append(allocator, buffer);
465 return buffer;
466 }
467
468 fn backendMinBufferAlignment(handle: gpu.BackendHandle) !u32 {
469 const caps = try handle.queryCapabilities();
470 return caps.memory.min_buffer_alignment;
471 }
472
473 fn backendAllocationAlignment(preferred: u64, min_buffer_alignment: u32) !u32 {
474 const aligned = @max(preferred, @as(u64, min_buffer_alignment));
475 return std.math.cast(u32, aligned) orelse return error.InvalidArtifact;
476 }
477
478 fn appendSlotBinding(
479 allocator: std.mem.Allocator,
480 slot_bindings: *std.ArrayList(SlotBinding),
481 slot_id: usize,
482 handle: gpu.BufferHandle,
483 ) !void {
484 try appendSlotBindingWithAccess(allocator, slot_bindings, slot_id, handle, .read_write);
485 }
486
487 fn appendSlotBindingWithAccess(
488 allocator: std.mem.Allocator,
489 slot_bindings: *std.ArrayList(SlotBinding),
490 slot_id: usize,
491 handle: gpu.BufferHandle,
492 access: gpu.BufferAccess,
493 ) !void {
494 try slot_bindings.append(allocator, .{
495 .slot_id = slot_id,
496 .binding = executableBackendBinding(handle, access),
497 });
498 }
499
500 fn executableBackendBinding(handle: gpu.BufferHandle, access: gpu.BufferAccess) gpu.BufferBinding {
501 return .{
502 .handle = handle,
503 .access = access,
504 .ownership = handle.ownership,
505 .byte_size = handle.byte_size,
506 };
507 }
508
509 fn planBufferHandleFor(plan_buffers: []const BackendPlanBuffer, allocation_id: usize) ?gpu.BufferHandle {
510 for (plan_buffers) |plan_buffer| {
511 if (plan_buffer.allocation_id == allocation_id) return plan_buffer.handle;
512 }
513 return null;
514 }
515
516 fn plannedSlotHasKernelUse(
517 artifact_plan: *const artifact_product.BackendArtifactPlan,
518 slot_id: usize,
519 ) bool {
520 for (artifact_plan.kernels.items) |kernel| {
521 if (kernel.output_slot_id == slot_id) return true;
522 for (kernel.input_slot_ids) |input_slot_id| {
523 if (input_slot_id == slot_id) return true;
524 }
525 }
526 return false;
527 }
528
529 pub fn bindingForSlot(
530 bindings: []const SlotBinding,
531 slot_id: usize,
532 access: gpu.BufferAccess,
533 ) gpu.BackendError!gpu.BufferBinding {
534 for (bindings) |slot_binding| {
535 if (slot_binding.slot_id != slot_id) continue;
536 return .{
537 .handle = slot_binding.binding.handle,
538 .access = access,
539 .ownership = slot_binding.binding.ownership,
540 .byte_size = slot_binding.binding.byte_size,
541 };
542 }
543 return error.InvalidBuffer;
544 }
545
546 pub fn countBinding(binding: gpu.BufferBinding) gpu.BufferBinding {
547 return .{
548 .handle = binding.handle,
549 .access = .read_only,
550 .ownership = binding.ownership,
551 .byte_size = binding.byte_size,
552 };
553 }
554
555 pub fn elementCountBufferForKernel(
556 bindings: []const ElementCountBufferBinding,
557 kernel_id: usize,
558 ) ?gpu.BufferBinding {
559 for (bindings) |binding| {
560 if (binding.kernel_id == kernel_id) return binding.binding;
561 }
562 return null;
563 }
564
565 test "borrowed bindings run a cpu fragment on caller memory without copies" {
566 try @import("../fixture/root.zig").requireNativeCpuArtifacts();
567 const fixture = @import("fixture.zig");
568 const fragment_mod = @import("fragment.zig");
569 const invocation_mod = @import("invocation.zig");
570 const allocator = std.testing.allocator;
571
572 var state = gpu.cpu.State.init(allocator);
573 defer state.deinit();
574 const handle = state.handle();
575 const module = try fixture.addSemanticModule(allocator, "borrowed_bindings_add");
576 const compiled = try fragment_mod.compileFragmentFromSemanticModule(
577 allocator,
578 handle,
579 module,
580 .{ .artifact_format = .cpu_object },
581 );
582 var fragment = try fragment_mod.loadFragment(allocator, handle, compiled, .{
583 .artifact_format = .cpu_object,
584 });
585 defer fragment.deinit();
586 const artifact_plan = compiled.artifactPlan();
587 const launch_plan = compiled.launchPlan();
588
589 const lhs = try allocator.alignedAlloc(f32, .@"64", 8);
590 defer allocator.free(lhs);
591 const rhs = try allocator.alignedAlloc(f32, .@"64", 8);
592 defer allocator.free(rhs);
593 const sum = try allocator.alignedAlloc(f32, .@"64", 8);
594 defer allocator.free(sum);
595 for (lhs, rhs, 0..) |*left, *right, index| {
596 left.* = @floatFromInt(index);
597 right.* = 0.25 * @as(f32, @floatFromInt(index));
598 }
599 const inputs = [_][]const u8{ std.mem.sliceAsBytes(lhs), std.mem.sliceAsBytes(rhs) };
600 const outputs = [_][]u8{std.mem.sliceAsBytes(sum)};
601
602 const bindings = try prepareBorrowed(
603 allocator,
604 handle,
605 artifact_plan,
606 launch_plan,
607 &inputs,
608 &outputs,
609 );
610 defer bindings.deinit();
611 try std.testing.expectEqual(@as(usize, 3), borrowedBufferCount(bindings));
612 try fragment.submitInvocationWithOptions(allocator, bindings, .{});
613 try fragment.completeInvocationWithOptions(.{});
614 try completeBorrowed(bindings, &outputs);
615
616 var copied: [8]f32 = undefined;
617 const copied_outputs = [_][]u8{std.mem.sliceAsBytes(&copied)};
618 try invocation_mod.run(fragment, allocator, allocator, &inputs, &copied_outputs);
619 try std.testing.expectEqualSlices(u8, copied_outputs[0], outputs[0]);
620
621 const overlapping = [_][]u8{std.mem.sliceAsBytes(lhs)};
622 try std.testing.expectError(
623 error.InvalidBuffer,
624 prepareBorrowed(allocator, handle, artifact_plan, launch_plan, &inputs, &overlapping),
625 );
626 const short = [_][]u8{std.mem.sliceAsBytes(sum)[0..4]};
627 try std.testing.expectError(
628 error.InvalidBuffer,
629 prepareBorrowed(allocator, handle, artifact_plan, launch_plan, &inputs, &short),
630 );
631 }