lib/accy/src/artifact/fingerprint.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 model = @import("model/root.zig");
  6 const plan_mod = @import("plan.zig");
  7 const preparation = @import("../preparation/root.zig");
  8 
  9 pub fn artifactPlan(value: *const plan_mod.BackendArtifactPlan) u64 {
 10     var hasher = choir.product.incremental.FingerprintBuilder{};
 11     hasher.updateBytes("accy.artifact.plan");
 12     hashBackendTargetProfile(&hasher, value.target_profile);
 13     hashU64(&hasher, @backingInt(value.backend_kind));
 14     hashU64(&hasher, @backingInt(value.format));
 15     hashU64(&hasher, value.slots.len);
 16     for (value.slots) |slot| hashPlannedSlot(&hasher, slot);
 17     hashU64(&hasher, value.input_slot_ids.len);
 18     for (value.input_slot_ids) |slot_id| hashU64(&hasher, slot_id);
 19     hashU64(&hasher, value.output_slot_ids.len);
 20     for (value.output_slot_ids) |slot_id| hashU64(&hasher, slot_id);
 21     hashU64(&hasher, value.kernels.items.len);
 22     for (value.kernels.items) |kernel| hashPlannedKernel(&hasher, kernel);
 23     hashU64(&hasher, value.total_kernel_ops);
 24     hashU64(&hasher, value.total_static_bytes);
 25     return hasher.finish();
 26 }
 27 
 28 pub fn kernelCallRegistry(value: ?*const model.KernelCallRegistry) u64 {
 29     var hasher = choir.product.incremental.FingerprintBuilder{};
 30     hasher.updateBytes("accy.artifact.kernel_call_registry");
 31     hashU64(&hasher, @intFromBool(value != null));
 32     const registry = value orelse return hasher.finish();
 33     hashU64(&hasher, registry.entries.len);
 34     for (registry.entries) |entry| hashKernelCallArtifact(&hasher, entry);
 35     return hasher.finish();
 36 }
 37 
 38 pub fn launchResourcePlan(value: plan_mod.LaunchResourcePlan) u64 {
 39     var hasher = choir.product.incremental.FingerprintBuilder{};
 40     hasher.updateBytes("accy.artifact.launch_resource_plan");
 41     hashLaunchResourcePlan(&hasher, value);
 42     return hasher.finish();
 43 }
 44 
 45 fn hashBackendTargetProfile(hasher: *choir.product.incremental.FingerprintBuilder, profile: anytype) void {
 46     hashU64(hasher, @backingInt(profile.backend_kind));
 47     hashU64(hasher, @backingInt(profile.artifact_format));
 48     hashU64(hasher, @backingInt(profile.math_tier));
 49     hashU64(hasher, profile.dtype_bits);
 50     hashU64(hasher, profile.feature_bits);
 51 }
 52 
 53 fn hashPlannedSlot(hasher: *choir.product.incremental.FingerprintBuilder, slot: plan_mod.PlannedSlot) void {
 54     hashU64(hasher, slot.slot_id);
 55     hashBufferRole(hasher, slot.role);
 56     hashU64(hasher, @backingInt(slot.dtype));
 57     hashU64(hasher, @backingInt(slot.memory_space));
 58     hashU64(hasher, @backingInt(slot.memory_access));
 59     hashU64(hasher, @backingInt(slot.boundary_transfer));
 60     hashU64(hasher, @backingInt(slot.layout_kind));
 61     hashU64(hasher, slot.dims.len);
 62     for (slot.dims) |dim| hashU64(hasher, @bitCast(dim));
 63     hashOptionalU64Slice(hasher, slot.element_strides);
 64     hashU64(hasher, slot.minor_to_major.len);
 65     for (slot.minor_to_major) |minor| hashU64(hasher, minor);
 66     hashOptionalU64(hasher, slot.element_count);
 67     hashOptionalU64(hasher, slot.byte_size);
 68     hashU64(hasher, slot.alignment);
 69     hashU64(hasher, @intFromBool(slot.contiguous));
 70     hashU64(hasher, @intFromBool(slot.static_layout));
 71     hashU64(hasher, slot.layout_fingerprint);
 72     hashBytes(hasher, slot.constant_payload);
 73 }
 74 
 75 fn hashBufferRole(hasher: *choir.product.incremental.FingerprintBuilder, role: anytype) void {
 76     hashU64(hasher, @intFromBool(role.input));
 77     hashU64(hasher, @intFromBool(role.output));
 78     hashU64(hasher, @intFromBool(role.temporary));
 79     hashU64(hasher, @intFromBool(role.constant));
 80 }
 81 
 82 fn hashPlannedKernel(hasher: *choir.product.incremental.FingerprintBuilder, kernel: plan_mod.PlannedKernel) void {
 83     hashPlannedKernelCompile(hasher, kernel.compile);
 84     hashU64(hasher, kernel.kernel_id);
 85     hashU64(hasher, kernel.work_item_id);
 86     hashU64(hasher, kernel.output_slot_id);
 87     hashU64(hasher, kernel.input_slot_ids.len);
 88     for (kernel.input_slot_ids) |slot_id| hashU64(hasher, slot_id);
 89     hashU64(hasher, kernel.output_layout_fingerprint);
 90     hashU64(hasher, kernel.input_layout_fingerprint);
 91     hashU64(hasher, kernel.element_count);
 92     hashU64(hasher, kernel.op_count);
 93     hashScheduleResourceEstimate(hasher, kernel.resources);
 94     hashKernelArtifact(hasher, kernel.artifact);
 95     hashLaunchResourcePlan(hasher, kernel.launch_resources);
 96     hashOptionalKernelCallLaunch(hasher, kernel.kernel_call_launch);
 97     hashU64(hasher, @backingInt(kernel.element_count_argument));
 98     hashU64(hasher, kernel.element_count_argument_value);
 99     hashU64(hasher, kernel.runtime_scalar_argument_count);
100     hashU64(hasher, kernel.runtime_scalar_defaults.len);
101     for (kernel.runtime_scalar_defaults) |argument| hashKernelScalarArgument(hasher, argument);
102     hashU64(hasher, kernel.static_arguments.len);
103     for (kernel.static_arguments) |argument| hashKernelScalarArgument(hasher, argument);
104 }
105 
106 fn hashPlannedKernelCompile(hasher: *choir.product.incremental.FingerprintBuilder, compile: plan_mod.PlannedKernelCompileContract) void {
107     hashU64(hasher, @backingInt(compile.source));
108     hashU64(hasher, @backingInt(compile.launch));
109     hashU64(hasher, @backingInt(compile.format));
110     hashBytes(hasher, compile.entry_name);
111     hashU64(hasher, compile.argument_count);
112     hashU64(hasher, compile.required_dtypes.bits);
113     hashAcceleratorFeatures(hasher, compile.required_features);
114     hashSubgroupRequirements(hasher, compile.required_subgroup);
115     hashOptionalU64(hasher, compile.shape_family_fingerprint);
116     hashU64(hasher, @backingInt(compile.payload));
117     hashU64(hasher, compile.payload_byte_count);
118 }
119 
120 fn hashAcceleratorFeatures(hasher: *choir.product.incremental.FingerprintBuilder, features: choir_abi.Features) void {
121     hashU64(hasher, @intFromBool(features.atomic_i32));
122     hashU64(hasher, @intFromBool(features.atomic_u32));
123     hashU64(hasher, @intFromBool(features.atomic_index));
124     hashU64(hasher, @intFromBool(features.atomic_f32_add_device));
125     hashU64(hasher, @intFromBool(features.atomic_f32_add_shared));
126     hashU64(hasher, @intFromBool(features.unsupported_atomic));
127     hashU64(hasher, @intFromBool(features.async_copy));
128     hashU64(hasher, @intFromBool(features.tensor_cores));
129     hashU64(hasher, @intFromBool(features.cooperative_matrix));
130     hashU64(hasher, @intFromBool(features.dynamic_shared_memory));
131     hashU64(hasher, @intFromBool(features.indirect_launch));
132 }
133 
134 fn hashSubgroupRequirements(hasher: *choir.product.incremental.FingerprintBuilder, requirements: choir_abi.SubgroupRequirements) void {
135     hashU64(hasher, @intFromBool(requirements.supported));
136     hashU64(hasher, requirements.size_min);
137     hashU64(hasher, requirements.size_max);
138     hashU64(hasher, @intFromBool(requirements.shuffle));
139     hashU64(hasher, @intFromBool(requirements.ballot));
140     hashU64(hasher, @intFromBool(requirements.vote));
141     hashU64(hasher, @intFromBool(requirements.arithmetic));
142     hashU64(hasher, @intFromBool(requirements.scan));
143 }
144 
145 fn hashScheduleResourceEstimate(hasher: *choir.product.incremental.FingerprintBuilder, resources: anytype) void {
146     hashU64(hasher, resources.element_count);
147     hashU64(hasher, resources.element_size);
148     hashU64(hasher, resources.op_count);
149     hashU64(hasher, resources.external_input_value_count);
150     hashU64(hasher, resources.external_operand_count);
151     hashU64(hasher, resources.chain_operand_count);
152     hashU64(hasher, resources.estimated_element_ops);
153     hashU64(hasher, resources.static_read_bytes);
154     hashU64(hasher, resources.static_write_bytes);
155     hashU64(hasher, resources.static_total_bytes);
156     hashU64(hasher, @intFromBool(resources.static_bytes_complete));
157 }
158 
159 fn hashKernelArtifact(hasher: *choir.product.incremental.FingerprintBuilder, artifact: gpu.KernelArtifact) void {
160     hashU64(hasher, @backingInt(artifact.backend));
161     hashU64(hasher, @backingInt(artifact.format));
162     hashBytes(hasher, artifact.entry_name);
163     hashU64(hasher, artifact.argument_count);
164     hashOptionalBytes(hasher, artifact.diagnostic_id);
165     hashArtifactPayload(hasher, artifact.payload);
166     hashU64(hasher, @backingInt(artifact.payload_ownership));
167 }
168 
169 fn hashArtifactPayload(hasher: *choir.product.incremental.FingerprintBuilder, payload: gpu.ArtifactPayload) void {
170     hashU64(hasher, @backingInt(payload));
171     switch (payload) {
172         .none => {},
173         .bytes => |bytes| hashBytes(hasher, bytes),
174         .text => |text| hashBytes(hasher, text),
175         .words_u32 => |words| {
176             hashU64(hasher, words.len);
177             for (words) |word| hashU64(hasher, word);
178         },
179         .external => |external| {
180             hashU64(hasher, @intFromPtr(external.ptr));
181             hashU64(hasher, @intFromBool(external.deinit_fn != null));
182         },
183     }
184 }
185 
186 fn hashCompilePayload(hasher: *choir.product.incremental.FingerprintBuilder, payload: gpu.CompilePayload) void {
187     hashU64(hasher, @backingInt(payload));
188     switch (payload) {
189         .none => {},
190         .bytes => |bytes| hashBytes(hasher, bytes),
191         .text => |text| hashBytes(hasher, text),
192         .words_u32 => |words| {
193             hashU64(hasher, words.len);
194             for (words) |word| hashU64(hasher, word);
195         },
196     }
197 }
198 
199 fn hashKernelCallArtifact(hasher: *choir.product.incremental.FingerprintBuilder, artifact: model.KernelCallArtifact) void {
200     hashBytes(hasher, artifact.target);
201     hashU64(hasher, artifact.version);
202     hashU64(hasher, @backingInt(artifact.format));
203     hashBytes(hasher, artifact.entry_name);
204     hashU64(hasher, artifact.argument_count);
205     hashOptionalU64(hasher, artifact.shape_family_fingerprint);
206     hashOptionalKernelCallShapeProfile(hasher, artifact.shape_profile);
207     hashU64(hasher, artifact.required_dtypes.bits);
208     hashAcceleratorFeatures(hasher, artifact.required_features);
209     hashSubgroupRequirements(hasher, artifact.required_subgroup);
210     hashCompilePayload(hasher, artifact.payload);
211     hashKernelCallLaunch(hasher, artifact.launch);
212     hashU64(hasher, @backingInt(artifact.element_count_argument));
213     hashU64(hasher, artifact.runtime_scalar_argument_count);
214     hashU64(hasher, artifact.static_arguments.len);
215     for (artifact.static_arguments) |argument| hashKernelScalarArgument(hasher, argument);
216 }
217 
218 fn hashOptionalKernelCallShapeProfile(hasher: *choir.product.incremental.FingerprintBuilder, profile: ?model.KernelCallShapeProfile) void {
219     hashU64(hasher, @intFromBool(profile != null));
220     if (profile) |value| {
221         hashBytes(hasher, value.name);
222         hashU64(hasher, value.fingerprint);
223         hashU64(hasher, value.dimensions.len);
224         for (value.dimensions) |dimension| {
225             hashBytes(hasher, dimension.name);
226             hashU64(hasher, dimension.runtime_scalar_argument_index);
227             hashShapeBounds(hasher, dimension.bounds);
228         }
229     }
230 }
231 
232 fn hashShapeBounds(hasher: *choir.product.incremental.FingerprintBuilder, bounds: anytype) void {
233     hashOptionalU64(hasher, bounds.min);
234     hashOptionalU64(hasher, bounds.opt);
235     hashOptionalU64(hasher, bounds.max);
236 }
237 
238 fn hashOptionalKernelCallLaunch(hasher: *choir.product.incremental.FingerprintBuilder, launch: ?model.KernelCallLaunch) void {
239     hashU64(hasher, @intFromBool(launch != null));
240     if (launch) |value| hashKernelCallLaunch(hasher, value);
241 }
242 
243 fn hashKernelCallLaunch(hasher: *choir.product.incremental.FingerprintBuilder, launch: model.KernelCallLaunch) void {
244     hashU64(hasher, @backingInt(launch));
245     switch (launch) {
246         .derived => |derived| hashKernelCallDerivedLaunch(hasher, derived),
247         .fixed => |geometry| hashLaunchGeometry(hasher, geometry),
248     }
249 }
250 
251 fn hashKernelCallDerivedLaunch(hasher: *choir.product.incremental.FingerprintBuilder, launch: model.KernelCallDerivedLaunch) void {
252     for (launch.grid) |axis| hashKernelCallDerivedLaunchAxis(hasher, axis);
253     for (launch.threadgroup) |extent| hashU64(hasher, extent);
254     hashU64(hasher, launch.dynamic_shared_memory_bytes);
255 }
256 
257 fn hashKernelCallDerivedLaunchAxis(hasher: *choir.product.incremental.FingerprintBuilder, axis: model.KernelCallDerivedLaunchAxis) void {
258     hashU64(hasher, @backingInt(axis));
259     switch (axis) {
260         .fixed => |extent| hashU64(hasher, extent),
261         .runtime_u32_ceil_div => |runtime| {
262             hashU64(hasher, runtime.argument_index);
263             hashU64(hasher, runtime.divisor);
264         },
265     }
266 }
267 
268 fn hashLaunchResourcePlan(hasher: *choir.product.incremental.FingerprintBuilder, plan: plan_mod.LaunchResourcePlan) void {
269     hashU64(hasher, @backingInt(plan.format));
270     hashU64(hasher, plan.element_count);
271     hashLaunchGeometry(hasher, plan.geometry);
272     hashOptionalU32(hasher, plan.subgroup_size);
273     hashU64(hasher, @intFromBool(plan.subgroup_aligned));
274     hashU64(hasher, @intFromBool(plan.fixed_threadgroup));
275     hashU64(hasher, @backingInt(plan.resource_class));
276     hashU64(hasher, plan.element_ops_per_kib);
277     hashU64(hasher, plan.estimated_static_bytes_per_threadgroup);
278     hashU64(hasher, plan.estimated_element_ops_per_threadgroup);
279     hashU64(hasher, @intFromBool(plan.static_bytes_complete));
280     hashLaunchTilePlan(hasher, plan.tile);
281     hashU64(hasher, plan.candidate_count);
282     for (plan.candidates[0..plan.candidate_count]) |candidate| hashLaunchResourceCandidate(hasher, candidate);
283 }
284 
285 fn hashLaunchResourceCandidate(hasher: *choir.product.incremental.FingerprintBuilder, candidate: plan_mod.LaunchResourceCandidate) void {
286     hashLaunchGeometry(hasher, candidate.geometry);
287     hashU64(hasher, candidate.score);
288     hashU64(hasher, candidate.estimated_static_bytes_per_threadgroup);
289     hashU64(hasher, candidate.estimated_element_ops_per_threadgroup);
290     hashLaunchTilePlan(hasher, candidate.tile);
291 }
292 
293 fn hashLaunchGeometry(hasher: *choir.product.incremental.FingerprintBuilder, geometry: choir_abi.LaunchGeometry) void {
294     for (geometry.grid) |value| hashU64(hasher, value);
295     for (geometry.threadgroup) |value| hashU64(hasher, value);
296     hashU64(hasher, geometry.dynamic_shared_memory_bytes);
297 }
298 
299 fn hashLaunchTilePlan(hasher: *choir.product.incremental.FingerprintBuilder, tile: plan_mod.LaunchTilePlan) void {
300     hashU64(hasher, @backingInt(tile.kind));
301     hashU64(hasher, tile.m);
302     hashU64(hasher, tile.n);
303     hashU64(hasher, tile.k);
304     hashU64(hasher, tile.batch);
305     hashOptionalDType(hasher, tile.input_dtype);
306     hashOptionalDType(hasher, tile.output_dtype);
307     hashU64(hasher, tile.input_tile_bytes);
308     hashU64(hasher, tile.output_tile_bytes);
309     hashU64(hasher, tile.scratch_memory_bytes);
310     hashU64(hasher, @backingInt(tile.reduction_kind));
311     hashU64(hasher, tile.reduction_rank);
312     hashU64(hasher, tile.reduction_axis);
313     hashU64(hasher, tile.reduction_extent);
314 }
315 
316 fn hashKernelScalarArgument(hasher: *choir.product.incremental.FingerprintBuilder, argument: choir_abi.ScalarArgument) void {
317     hashU64(hasher, @backingInt(argument));
318     switch (argument) {
319         .i32 => |value| hashU64(hasher, @as(u32, @bitCast(value))),
320         .u32 => |value| hashU64(hasher, value),
321         .i64 => |value| hashU64(hasher, @bitCast(value)),
322         .u64 => |value| hashU64(hasher, value),
323         .f32 => |value| hashU64(hasher, @as(u32, @bitCast(value))),
324         .f64 => |value| hashU64(hasher, @bitCast(value)),
325     }
326 }
327 
328 fn hashOptionalDType(hasher: *choir.product.incremental.FingerprintBuilder, dtype: ?choir_abi.DType) void {
329     hasher.updateBool(dtype != null);
330     if (dtype) |value| hashU64(hasher, @backingInt(value));
331 }
332 
333 fn hashOptionalU32(hasher: *choir.product.incremental.FingerprintBuilder, value: ?u32) void {
334     hasher.updateBool(value != null);
335     if (value) |payload| hasher.updateU32(payload);
336 }
337 
338 fn hashOptionalU64(hasher: *choir.product.incremental.FingerprintBuilder, value: ?u64) void {
339     hasher.updateOptionalU64(value);
340 }
341 
342 fn hashOptionalU64Slice(hasher: *choir.product.incremental.FingerprintBuilder, value: ?[]const u64) void {
343     hasher.updateOptionalU64Slice(value);
344 }
345 
346 fn hashOptionalBytes(hasher: *choir.product.incremental.FingerprintBuilder, value: ?[]const u8) void {
347     hasher.updateBool(value != null);
348     if (value) |bytes| hashBytes(hasher, bytes);
349 }
350 
351 fn hashBytes(hasher: *choir.product.incremental.FingerprintBuilder, bytes: []const u8) void {
352     hasher.updateBytes(bytes);
353 }
354 
355 fn hashU64(hasher: *choir.product.incremental.FingerprintBuilder, value: u64) void {
356     hasher.updateU64(value);
357 }
358 
359 test "artifact plan fingerprint includes backend target profile" {
360     const allocator = std.testing.allocator;
361 
362     var f32_plan = plan_mod.BackendArtifactPlan.init(allocator, .{
363         .backend_kind = .cuda,
364         .artifact_format = .cuda_ptx,
365         .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
366     });
367     defer f32_plan.deinit();
368 
369     var f16_f32_plan = plan_mod.BackendArtifactPlan.init(allocator, .{
370         .backend_kind = .cuda,
371         .artifact_format = .cuda_ptx,
372         .dtype_bits = gpu.DTypeSet.init(&.{ .f16, .f32 }).bits,
373     });
374     defer f16_f32_plan.deinit();
375 
376     const tf32_profile = try preparation.BackendTargetProfile.initWithMathTier(.{
377         .identity = .{
378             .backend = .cuda,
379             .family = .nvidia_cuda,
380         },
381         .dtypes = gpu.DTypeSet.init(&.{.f32}),
382         .features = .{ .tensor_cores = true },
383         .artifact_formats = gpu.ArtifactFormatSet.init(&.{.cuda_ptx}),
384     }, .cuda, .cuda_ptx, .tf32_tensor);
385     var tf32_plan = plan_mod.BackendArtifactPlan.init(allocator, tf32_profile);
386     defer tf32_plan.deinit();
387 
388     try std.testing.expect(artifactPlan(&f32_plan) != artifactPlan(&f16_f32_plan));
389     try std.testing.expect(artifactPlan(&f32_plan) != artifactPlan(&tf32_plan));
390 }
391 
392 test "artifact plan fingerprint includes shape family compile identity" {
393     const allocator = std.testing.allocator;
394 
395     var first = try standalonePlanWithShapeFamilyFingerprint(allocator, 0x1111);
396     defer first.deinit();
397     var same = try standalonePlanWithShapeFamilyFingerprint(allocator, 0x1111);
398     defer same.deinit();
399     var changed = try standalonePlanWithShapeFamilyFingerprint(allocator, 0x2222);
400     defer changed.deinit();
401 
402     try std.testing.expectEqual(artifactPlan(&first), artifactPlan(&same));
403     try std.testing.expect(artifactPlan(&first) != artifactPlan(&changed));
404 }
405 
406 fn standalonePlanWithShapeFamilyFingerprint(allocator: std.mem.Allocator, family_fingerprint: u64) !plan_mod.BackendArtifactPlan {
407     var plan = plan_mod.BackendArtifactPlan.init(allocator, .{
408         .backend_kind = .cuda,
409         .artifact_format = .cuda_ptx,
410         .dtype_bits = gpu.DTypeSet.init(&.{.f32}).bits,
411     });
412     errdefer plan.deinit();
413 
414     var artifact = try gpu.KernelArtifact.init(allocator, .{
415         .backend = .cuda,
416         .format = .cuda_ptx,
417         .entry_name = "kernel_call",
418         .argument_count = 3,
419     });
420     var artifact_owned = true;
421     errdefer if (artifact_owned) artifact.deinit();
422     try artifact.setOwnedText("// ptx");
423 
424     var compile = try plan_mod.PlannedKernelCompileContract.init(
425         allocator,
426         .kernel_call,
427         .kernel_call,
428         .cuda_ptx,
429         "kernel_call",
430         3,
431         gpu.DTypeSet.init(&.{.f32}),
432         .{},
433         .{},
434         family_fingerprint,
435         .{ .text = "// ptx" },
436     );
437     var compile_owned = true;
438     errdefer if (compile_owned) compile.deinit(allocator);
439 
440     try plan.addStandaloneKernel(
441         artifact,
442         .{
443             .format = .cuda_ptx,
444             .element_count = 1,
445             .geometry = .{
446                 .grid = .{ 1, 1, 1 },
447                 .threadgroup = .{ 1, 1, 1 },
448             },
449         },
450         compile,
451         .{},
452     );
453     artifact_owned = false;
454     compile_owned = false;
455 
456     return plan;
457 }
458 
459 test "kernel call registry fingerprint includes payload and launch contract" {
460     const ptx = ".visible .entry first() { ret; }";
461     const other_ptx = ".visible .entry second() { ret; }";
462     const first_entries = [_]model.KernelCallArtifact{.{
463         .target = "accy.custom.scale",
464         .version = 1,
465         .format = .cuda_ptx,
466         .entry_name = "first",
467         .argument_count = 5,
468         .required_dtypes = gpu.DTypeSet.init(&.{.f32}),
469         .payload = .{ .text = ptx },
470         .launch = .{ .fixed = .{
471             .grid = .{ 1, 1, 1 },
472             .threadgroup = .{ 8, 1, 1 },
473             .dynamic_shared_memory_bytes = 0,
474         } },
475         .element_count_argument = .scalar_u32,
476         .shape_family_fingerprint = 0xaaaa,
477         .static_arguments = &.{.{ .u32 = 7 }},
478     }};
479     const first = model.KernelCallRegistry{ .entries = &first_entries };
480     const first_again = model.KernelCallRegistry{ .entries = &first_entries };
481     const changed_payload_entries = [_]model.KernelCallArtifact{.{
482         .target = "accy.custom.scale",
483         .version = 1,
484         .format = .cuda_ptx,
485         .entry_name = "first",
486         .argument_count = 5,
487         .required_dtypes = gpu.DTypeSet.init(&.{.f32}),
488         .payload = .{ .text = other_ptx },
489         .launch = .{ .fixed = .{
490             .grid = .{ 1, 1, 1 },
491             .threadgroup = .{ 8, 1, 1 },
492             .dynamic_shared_memory_bytes = 0,
493         } },
494         .element_count_argument = .scalar_u32,
495         .shape_family_fingerprint = 0xaaaa,
496         .static_arguments = &.{.{ .u32 = 7 }},
497     }};
498     const changed_payload = model.KernelCallRegistry{ .entries = &changed_payload_entries };
499     const changed_launch_entries = [_]model.KernelCallArtifact{.{
500         .target = "accy.custom.scale",
501         .version = 1,
502         .format = .cuda_ptx,
503         .entry_name = "first",
504         .argument_count = 5,
505         .required_dtypes = gpu.DTypeSet.init(&.{.f32}),
506         .payload = .{ .text = ptx },
507         .launch = .{ .fixed = .{
508             .grid = .{ 2, 1, 1 },
509             .threadgroup = .{ 8, 1, 1 },
510             .dynamic_shared_memory_bytes = 0,
511         } },
512         .element_count_argument = .scalar_u32,
513         .shape_family_fingerprint = 0xaaaa,
514         .static_arguments = &.{.{ .u32 = 7 }},
515     }};
516     const changed_launch = model.KernelCallRegistry{ .entries = &changed_launch_entries };
517     const changed_shape_family_entries = [_]model.KernelCallArtifact{.{
518         .target = "accy.custom.scale",
519         .version = 1,
520         .format = .cuda_ptx,
521         .entry_name = "first",
522         .argument_count = 5,
523         .required_dtypes = gpu.DTypeSet.init(&.{.f32}),
524         .payload = .{ .text = ptx },
525         .launch = .{ .fixed = .{
526             .grid = .{ 1, 1, 1 },
527             .threadgroup = .{ 8, 1, 1 },
528             .dynamic_shared_memory_bytes = 0,
529         } },
530         .element_count_argument = .scalar_u32,
531         .shape_family_fingerprint = 0xbbbb,
532         .static_arguments = &.{.{ .u32 = 7 }},
533     }};
534     const changed_shape_family = model.KernelCallRegistry{ .entries = &changed_shape_family_entries };
535 
536     try std.testing.expectEqual(kernelCallRegistry(&first), kernelCallRegistry(&first_again));
537     try std.testing.expect(kernelCallRegistry(&first) != kernelCallRegistry(null));
538     try std.testing.expect(kernelCallRegistry(&first) != kernelCallRegistry(&changed_payload));
539     try std.testing.expect(kernelCallRegistry(&first) != kernelCallRegistry(&changed_launch));
540     try std.testing.expect(kernelCallRegistry(&first) != kernelCallRegistry(&changed_shape_family));
541 }