lib/accy/src/kernel/library/random/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gpu = @import("gpu");
3 const choir_abi = @import("choir_abi");
4 const base = @import("base.zig");
5 const random = @import("root.zig");
6
7 test {
8 @import("test_discovery").discover(random);
9 @import("test_discovery").discover(@This());
10 }
11
12 const entry = base.entry;
13 const kernel = base.kernel;
14 const tuning = base.tuning;
15
16 const Philox = random.Philox;
17 const Threefry = random.Threefry;
18 const Squares = random.Squares;
19 const Feistel = random.Feistel;
20 const PhiloxFold = random.PhiloxFold;
21 const ThreefryFold = random.ThreefryFold;
22 const SquaresFold = random.SquaresFold;
23 const PhiloxKeySplit = random.PhiloxKeySplit;
24 const PhiloxKeyUniform = random.PhiloxKeyUniform;
25 const PhiloxKeyCounterUniform = random.PhiloxKeyCounterUniform;
26 const Philox8F32 = random.Philox8F32;
27 const Threefry8F32 = random.Threefry8F32;
28 const PhiloxRuntimeFamilyI32 = random.PhiloxRuntimeFamilyI32;
29 const ThreefryRuntimeFamilyI32 = random.ThreefryRuntimeFamilyI32;
30 const SquaresRuntimeFamilyI32 = random.SquaresRuntimeFamilyI32;
31 const FeistelRuntimeFamilyI32 = random.FeistelRuntimeFamilyI32;
32 const PhiloxKeySplitRuntimeFamily = random.PhiloxKeySplitRuntimeFamily;
33 const PhiloxKeyUniformRuntimeFamilyF32 = random.PhiloxKeyUniformRuntimeFamilyF32;
34 const PhiloxKeyCounterUniformRuntimeFamilyF32 = random.PhiloxKeyCounterUniformRuntimeFamilyF32;
35 const PhiloxFoldRuntimeFamilyF32 = random.PhiloxFoldRuntimeFamilyF32;
36 const ThreefryFoldRuntimeFamilyI32 = random.ThreefryFoldRuntimeFamilyI32;
37 const SquaresFoldRuntimeFamilyF32 = random.SquaresFoldRuntimeFamilyF32;
38 const philox_lanes = random.philox_lanes;
39 const philox_default_rounds = random.philox_default_rounds;
40 const philox_family_version = random.philox_family_version;
41 const threefry_lanes = random.threefry_lanes;
42 const threefry_default_rounds = random.threefry_default_rounds;
43 const squares_default_key = random.squares_default_key;
44 const philoxBlock = random.philoxBlock;
45 const threefryBlock = random.threefryBlock;
46 const squaresBlock = random.squaresBlock;
47 const uniformFromBits = random.uniformFromBits;
48 const philoxInstanceTarget = random.philoxInstanceTarget;
49 const philoxInstanceEntryName = random.philoxInstanceEntryName;
50 const philoxFamilyTarget = random.philoxFamilyTarget;
51 const threefryInstanceTarget = random.threefryInstanceTarget;
52 const threefryFamilyTarget = random.threefryFamilyTarget;
53 const philoxFamilyTuningKey = random.philoxFamilyTuningKey;
54 const threefryFamilyTuningKey = random.threefryFamilyTuningKey;
55 const squaresFamilyTuningKey = random.squaresFamilyTuningKey;
56 const createPhiloxFamilyArtifact = random.createPhiloxFamilyArtifact;
57 const createPhiloxKeySplitFamilyArtifact = random.createPhiloxKeySplitFamilyArtifact;
58 const createPhiloxKeyUniformFamilyArtifact = random.createPhiloxKeyUniformFamilyArtifact;
59 const createPhiloxKeyCounterUniformFamilyArtifact = random.createPhiloxKeyCounterUniformFamilyArtifact;
60 const philoxFoldUniformReference = random.philoxFoldUniformReference;
61 const threefryFoldBitsReference = random.threefryFoldBitsReference;
62 const createPhiloxFoldFamilyArtifact = random.createPhiloxFoldFamilyArtifact;
63 const philoxFoldRuntimeArguments = random.philoxFoldRuntimeArguments;
64 const philoxFoldFamilyTuningKey = random.philoxFoldFamilyTuningKey;
65 const threefryFoldFamilyTuningKey = random.threefryFoldFamilyTuningKey;
66 const squaresFoldFamilyTuningKey = random.squaresFoldFamilyTuningKey;
67 const squaresFoldUniformReference = random.squaresFoldUniformReference;
68 const createSquaresFamilyArtifact = random.createSquaresFamilyArtifact;
69 const squaresFamilySpecialization = random.squaresFamilySpecialization;
70 const squaresInstanceFromSpecialization = random.squaresInstanceFromSpecialization;
71 const philoxInstanceFromSpecialization = random.philoxInstanceFromSpecialization;
72 const threefryInstanceFromSpecialization = random.threefryInstanceFromSpecialization;
73 const philoxFamilySpecialization = random.philoxFamilySpecialization;
74 const threefryFamilySpecialization = random.threefryFamilySpecialization;
75 const feistelFamilyTarget = random.feistelFamilyTarget;
76 const feistelFamilyEntryName = random.feistelFamilyEntryName;
77 const feistelPermuteReference = random.feistelPermuteReference;
78 const feistelInstanceValid = random.feistelInstanceValid;
79 const feistelDomainBits = random.feistelDomainBits;
80 const feistelRuntimeArguments = random.feistelRuntimeArguments;
81 const Key = choir_abi.Key;
82
83 fn randomFamilyTuningTestCapabilities(device_id: u32) gpu.BackendCapabilities {
84 return .{ .identity = .{
85 .backend = .cuda,
86 .family = .nvidia_cuda,
87 .name = "random-family-tuning-test-device",
88 .vendor_id = 0x10de,
89 .device_id = device_id,
90 } };
91 }
92
93 test "random philox reference matches Random123 known answers" {
94 try std.testing.expectEqual(
95 [4]u32{ 0x6627e8d5, 0xe169c58d, 0xbc57ac4c, 0x9b00dbd8 },
96 philoxBlock(10, .{ 0, 0, 0, 0 }, .{ 0, 0 }),
97 );
98 try std.testing.expectEqual(
99 [4]u32{ 0x408f276d, 0x41c83b0e, 0xa20bc7c6, 0x6d5451fd },
100 philoxBlock(10, .{ 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff }, .{ 0xffffffff, 0xffffffff }),
101 );
102 try std.testing.expectEqual(
103 [4]u32{ 0xd16cfe09, 0x94fdcceb, 0x5001e420, 0x24126ea1 },
104 philoxBlock(10, .{ 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344 }, .{ 0xa4093822, 0x299f31d0 }),
105 );
106 try std.testing.expectEqual(
107 [4]u32{ 0x5f6fb709, 0x0d893f64, 0x4f121f81, 0x4f730a48 },
108 philoxBlock(7, .{ 0, 0, 0, 0 }, .{ 0, 0 }),
109 );
110 try std.testing.expectEqual(
111 [4]u32{ 0x5207ddc2, 0x45165e59, 0x4d8ee751, 0x8c52f662 },
112 philoxBlock(7, .{ 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff }, .{ 0xffffffff, 0xffffffff }),
113 );
114 try std.testing.expectEqual(
115 [4]u32{ 0x4dfccaba, 0x190a87f0, 0xc47362ba, 0xb6b5242a },
116 philoxBlock(7, .{ 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344 }, .{ 0xa4093822, 0x299f31d0 }),
117 );
118 }
119
120 test "random threefry reference matches Random123 known answers" {
121 try std.testing.expectEqual(
122 [2]u32{ 0x6b200159, 0x99ba4efe },
123 threefryBlock(20, .{ 0, 0 }, .{ 0, 0 }),
124 );
125 try std.testing.expectEqual(
126 [2]u32{ 0x1cb996fc, 0xbb002be7 },
127 threefryBlock(20, .{ 0xffffffff, 0xffffffff }, .{ 0xffffffff, 0xffffffff }),
128 );
129 try std.testing.expectEqual(
130 [2]u32{ 0xc4923a9c, 0x483df7a0 },
131 threefryBlock(20, .{ 0x243f6a88, 0x85a308d3 }, .{ 0x13198a2e, 0x03707344 }),
132 );
133 try std.testing.expectEqual(
134 [2]u32{ 0x9d1c5ec6, 0x8bd50731 },
135 threefryBlock(13, .{ 0, 0 }, .{ 0, 0 }),
136 );
137 try std.testing.expectEqual(
138 [2]u32{ 0xfd36d048, 0x2d17272c },
139 threefryBlock(13, .{ 0xffffffff, 0xffffffff }, .{ 0xffffffff, 0xffffffff }),
140 );
141 try std.testing.expectEqual(
142 [2]u32{ 0xba3e4725, 0xf27d669e },
143 threefryBlock(13, .{ 0x243f6a88, 0x85a308d3 }, .{ 0x13198a2e, 0x03707344 }),
144 );
145 }
146
147 test "random uniform conversion stays in the unit interval" {
148 try std.testing.expectEqual(@as(f32, 0.0), uniformFromBits(0));
149 const top = uniformFromBits(0xffffffff);
150 try std.testing.expect(top < 1.0);
151 try std.testing.expect(top >= 0.0);
152 const mid = uniformFromBits(0x80000000);
153 try std.testing.expectApproxEqAbs(@as(f32, 0.5), mid, 0.0000002);
154 }
155
156 test "random philox entry writes Random123 words through the uniform map on CPU" {
157 var dst = @as([8]f32, @splat(0));
158
159 try Philox8F32.runCpu(std.testing.allocator, Philox8F32.Limits.testing, &.{
160 kernel.argumentBuffer(f32, dst[0..]),
161 kernel.argumentI32(0),
162 kernel.argumentI32(0),
163 });
164
165 var expected: [8]f32 = undefined;
166 for (0..2) |generator| {
167 const words = philoxBlock(philox_default_rounds, .{ @intCast(generator), 0, 0, 0 }, .{ 0, 0 });
168 for (words, 0..) |word, lane| expected[generator * 4 + lane] = uniformFromBits(word);
169 }
170 try std.testing.expectEqualSlices(f32, expected[0..], dst[0..]);
171 }
172
173 test "random threefry entry matches reference blocks on CPU" {
174 var dst = @as([8]f32, @splat(0));
175
176 try Threefry8F32.runCpu(std.testing.allocator, Threefry8F32.Limits.testing, &.{
177 kernel.argumentBuffer(f32, dst[0..]),
178 kernel.argumentI32(7),
179 kernel.argumentI32(11),
180 });
181
182 var expected: [8]f32 = undefined;
183 for (0..4) |generator| {
184 const words = threefryBlock(threefry_default_rounds, .{ @intCast(generator), 0 }, .{ 7, 11 });
185 for (words, 0..) |word, lane| expected[generator * 2 + lane] = uniformFromBits(word);
186 }
187 try std.testing.expectEqualSlices(f32, expected[0..], dst[0..]);
188 }
189
190 test "random philox runtime family emits raw bits with tail guard" {
191 const allocator = std.testing.allocator;
192 const compiled = Philox{ .count = 1, .dtype = .i32, .threads = 4 };
193 const runtime = Philox{ .count = 10, .dtype = .i32, .threads = 4, .seed = 0xdeadbeef00c0ffee };
194
195 var graph = try PhiloxRuntimeFamilyI32.build(allocator, PhiloxRuntimeFamilyI32.Limits.testing, compiled);
196 defer graph.deinit();
197
198 var dst = @as([12]i32, @splat(0));
199
200 const launch_value = try entry.runtimeLaunch1D(runtime.generators(), runtime.threads);
201 try graph.runCpuWithLaunch(allocator, &.{
202 kernel.argumentBuffer(i32, dst[0..]),
203 kernel.argumentI32(@intCast(runtime.count)),
204 kernel.argumentI32(@bitCast(runtime.seedLo())),
205 kernel.argumentI32(@bitCast(runtime.seedHi())),
206 }, .{
207 .grid = launch_value.grid,
208 .block = launch_value.threadgroup,
209 });
210
211 var expected = @as([12]i32, @splat(0));
212 for (0..3) |generator| {
213 const words = philoxBlock(
214 philox_default_rounds,
215 .{ @intCast(generator), 0, 0, 0 },
216 .{ runtime.seedLo(), runtime.seedHi() },
217 );
218 for (words, 0..) |word, lane| {
219 const element = generator * 4 + lane;
220 if (element < runtime.count) expected[element] = @bitCast(word);
221 }
222 }
223 try std.testing.expectEqualSlices(i32, expected[0..], dst[0..]);
224 }
225
226 test "random philox key families split keys and consume key tensors on CPU" {
227 const allocator = std.testing.allocator;
228 const split_instance = PhiloxKeySplit{ .count = 8, .threads = 4, .rounds = 10 };
229 const uniform_instance = PhiloxKeyUniform{ .count = 8, .threads = 4, .rounds = 10 };
230 const seed = Key.init(0x01234567_89abcdef);
231
232 var split_graph = try PhiloxKeySplitRuntimeFamily.build(allocator, PhiloxKeySplitRuntimeFamily.Limits.testing, split_instance);
233 defer split_graph.deinit();
234 var uniform_graph = try PhiloxKeyUniformRuntimeFamilyF32.build(allocator, PhiloxKeyUniformRuntimeFamilyF32.Limits.testing, uniform_instance);
235 defer uniform_graph.deinit();
236
237 var src = [_]Key{seed};
238 var keys = @as([8]Key, @splat(Key.init(0)));
239 const split_launch = try entry.runtimeLaunch1D(split_instance.generators(), split_instance.threads);
240 try split_graph.runCpuWithLaunch(allocator, &.{
241 kernel.argumentBuffer(Key, keys[0..]),
242 kernel.argumentBuffer(Key, src[0..]),
243 kernel.argumentI32(@intCast(split_instance.count)),
244 }, .{
245 .grid = split_launch.grid,
246 .block = split_launch.threadgroup,
247 });
248
249 for (keys, 0..) |key_value, index| {
250 const words = philoxBlock(
251 split_instance.rounds,
252 .{ @intCast(index), 0, 0, 0 },
253 .{ seed.lo(), seed.hi() },
254 );
255 try std.testing.expectEqual(Key.fromWords(words[0], words[1]), key_value);
256 }
257
258 var dst = @as([8]f32, @splat(-1.0));
259 const uniform_launch = try entry.runtimeLaunch1D(uniform_instance.generators(), uniform_instance.threads);
260 try uniform_graph.runCpuWithLaunch(allocator, &.{
261 kernel.argumentBuffer(f32, dst[0..]),
262 kernel.argumentBuffer(Key, keys[0..]),
263 kernel.argumentI32(@intCast(uniform_instance.count)),
264 }, .{
265 .grid = uniform_launch.grid,
266 .block = uniform_launch.threadgroup,
267 });
268
269 for (keys, 0..) |key_value, index| {
270 const words = philoxBlock(
271 uniform_instance.rounds,
272 .{ 0, 0, 0, 0 },
273 .{ key_value.lo(), key_value.hi() },
274 );
275 try std.testing.expectEqual(uniformFromBits(words[0]), dst[index]);
276 try std.testing.expect(dst[index] >= 0.0);
277 try std.testing.expect(dst[index] < 1.0);
278 }
279 }
280
281 test "random threefry runtime family emits raw bits with tail guard" {
282 const allocator = std.testing.allocator;
283 const compiled = Threefry{ .count = 1, .dtype = .i32, .threads = 4 };
284 const runtime = Threefry{ .count = 7, .dtype = .i32, .threads = 4, .seed = 0x0123456789abcdef };
285
286 var graph = try ThreefryRuntimeFamilyI32.build(allocator, ThreefryRuntimeFamilyI32.Limits.testing, compiled);
287 defer graph.deinit();
288
289 var dst = @as([8]i32, @splat(0));
290
291 const launch_value = try entry.runtimeLaunch1D(runtime.generators(), runtime.threads);
292 try graph.runCpuWithLaunch(allocator, &.{
293 kernel.argumentBuffer(i32, dst[0..]),
294 kernel.argumentI32(@intCast(runtime.count)),
295 kernel.argumentI32(@bitCast(runtime.seedLo())),
296 kernel.argumentI32(@bitCast(runtime.seedHi())),
297 }, .{
298 .grid = launch_value.grid,
299 .block = launch_value.threadgroup,
300 });
301
302 var expected = @as([8]i32, @splat(0));
303 for (0..4) |generator| {
304 const words = threefryBlock(
305 threefry_default_rounds,
306 .{ @intCast(generator), 0 },
307 .{ runtime.seedLo(), runtime.seedHi() },
308 );
309 for (words, 0..) |word, lane| {
310 const element = generator * 2 + lane;
311 if (element < runtime.count) expected[element] = @bitCast(word);
312 }
313 }
314 try std.testing.expectEqualSlices(i32, expected[0..], dst[0..]);
315 }
316
317 test "random feistel validates only balanced power-of-two i32 domains" {
318 try std.testing.expectEqual(@as(?u6, 6), feistelDomainBits(64));
319 try std.testing.expectEqual(@as(?u6, null), feistelDomainBits(63));
320 try std.testing.expectEqual(@as(?u6, null), feistelDomainBits(128));
321 try std.testing.expect(feistelInstanceValid(.{ .count = 64, .threads = 32 }));
322 try std.testing.expect(!feistelInstanceValid(.{ .count = 128, .threads = 32 }));
323 try std.testing.expect(!feistelInstanceValid(.{ .count = 64, .threads = 0 }));
324 try std.testing.expect(!feistelInstanceValid(.{ .count = 64, .rounds = 0, .threads = 32 }));
325 try std.testing.expect(!feistelInstanceValid(.{ .count = 64, .dtype = .f32, .threads = 32 }));
326 }
327
328 test "random feistel reference is bijective on balanced domain" {
329 const count = 64;
330 const bits = feistelDomainBits(count).?;
331 var seen = @as([count]bool, @splat(false));
332 for (0..count) |index| {
333 const mapped = feistelPermuteReference(@intCast(index), 6, 0x10203040_55667788, bits);
334 try std.testing.expect(mapped < count);
335 try std.testing.expect(!seen[mapped]);
336 seen[mapped] = true;
337 }
338 for (seen) |hit| try std.testing.expect(hit);
339 }
340
341 test "random feistel runtime family emits permutation stream on CPU" {
342 const allocator = std.testing.allocator;
343 const instance = Feistel{ .count = 64, .rounds = 6, .seed = 0x10203040_55667788, .threads = 32 };
344
345 var graph = try FeistelRuntimeFamilyI32.build(allocator, FeistelRuntimeFamilyI32.Limits.testing, instance);
346 defer graph.deinit();
347
348 var dst = @as([65]i32, @splat(-1));
349
350 const launch_value = try entry.runtimeLaunch1D(instance.count, instance.threads);
351 try graph.runCpuWithLaunch(allocator, &.{
352 kernel.argumentBuffer(i32, dst[0..]),
353 kernel.argumentI32(@intCast(instance.count)),
354 kernel.argumentI32(@bitCast(instance.seedLo())),
355 kernel.argumentI32(@bitCast(instance.seedHi())),
356 }, .{
357 .grid = launch_value.grid,
358 .block = launch_value.threadgroup,
359 });
360
361 var expected = @as([65]i32, @splat(-1));
362 const bits = instance.domainBits().?;
363 for (0..instance.count) |element| {
364 expected[element] = @intCast(feistelPermuteReference(@intCast(element), instance.rounds, instance.seed, bits));
365 }
366 try std.testing.expectEqualSlices(i32, expected[0..], dst[0..]);
367 }
368
369 test "random feistel family identity is stable" {
370 const instance = Feistel{ .count = 64, .rounds = 6, .seed = 0x10203040_55667788, .threads = 32 };
371
372 const target = try feistelFamilyTarget(std.testing.allocator, instance);
373 defer std.testing.allocator.free(target);
374 try std.testing.expectEqualStrings("accy.kernel.random.feistel_family_6b_6r_32_i32", target);
375
376 const entry_name = try feistelFamilyEntryName(std.testing.allocator, instance);
377 defer std.testing.allocator.free(entry_name);
378 try std.testing.expectEqualStrings("accy_kernel_random_feistel_family_6b_6r_32_i32", entry_name);
379
380 const args = try feistelRuntimeArguments(instance);
381 try std.testing.expectEqual(@as(u32, 64), args[0].u32);
382 try std.testing.expectEqual(instance.seedLo(), args[1].u32);
383 try std.testing.expectEqual(instance.seedHi(), args[2].u32);
384
385 try std.testing.expectEqual(@as(u32, 1), random.feistel_family_version);
386 }
387
388 test "random family instance identity matches fixed entry strings" {
389 const philox_instance = Philox{ .count = 8, .threads = 2 };
390
391 const philox_target = try philoxInstanceTarget(std.testing.allocator, philox_instance);
392 defer std.testing.allocator.free(philox_target);
393 try std.testing.expectEqualStrings(Philox8F32.target, philox_target);
394
395 const philox_entry_name = try philoxInstanceEntryName(std.testing.allocator, philox_instance);
396 defer std.testing.allocator.free(philox_entry_name);
397 try std.testing.expectEqualStrings(Philox8F32.name, philox_entry_name);
398
399 try std.testing.expectEqual(Philox8F32.version, philox_family_version);
400
401 const fresh = Philox{ .count = 1 << 20, .rounds = 7, .threads = 128 };
402 const family_target = try philoxFamilyTarget(std.testing.allocator, fresh);
403 defer std.testing.allocator.free(family_target);
404 try std.testing.expectEqualStrings("accy.kernel.random.philox_family_7r_128_f32", family_target);
405
406 const threefry_instance = Threefry{ .count = 8, .threads = 4 };
407 const threefry_target = try threefryInstanceTarget(std.testing.allocator, threefry_instance);
408 defer std.testing.allocator.free(threefry_target);
409 try std.testing.expectEqualStrings(Threefry8F32.target, threefry_target);
410
411 const threefry_family_target = try threefryFamilyTarget(std.testing.allocator, .{ .count = 64, .rounds = 13, .threads = 64, .dtype = .i32 });
412 defer std.testing.allocator.free(threefry_family_target);
413 try std.testing.expectEqualStrings("accy.kernel.random.threefry_family_13r_64_i32", threefry_family_target);
414 }
415
416 test "random family tuning keys distinguish generators rounds and device" {
417 const allocator = std.testing.allocator;
418 const device = tuning.deviceFingerprint(randomFamilyTuningTestCapabilities(0x2684));
419
420 const keys = [_]tuning.FamilyTuningKey{
421 try philoxFamilyTuningKey(allocator, device, .{ .count = 1024 }),
422 try threefryFamilyTuningKey(allocator, device, .{ .count = 1024 }),
423 try squaresFamilyTuningKey(allocator, device, .{ .count = 1024 }),
424 };
425 for (keys, 0..) |key, index| {
426 for (keys[index + 1 ..]) |other| {
427 try std.testing.expect(!key.eql(other));
428 }
429 }
430
431 const fewer_rounds = try philoxFamilyTuningKey(allocator, device, .{ .count = 1024, .rounds = 7 });
432 try std.testing.expect(!fewer_rounds.eql(keys[0]));
433 try std.testing.expectEqual(keys[0].family_fingerprint, fewer_rounds.family_fingerprint);
434 try std.testing.expect(keys[0].operation_fingerprint != fewer_rounds.operation_fingerprint);
435
436 const other_device = try philoxFamilyTuningKey(
437 allocator,
438 tuning.deviceFingerprint(randomFamilyTuningTestCapabilities(0x1b80)),
439 .{ .count = 1024 },
440 );
441 try std.testing.expect(!other_device.eql(keys[0]));
442 try std.testing.expectEqual(keys[0].family_fingerprint, other_device.family_fingerprint);
443 try std.testing.expectEqual(keys[0].operation_fingerprint, other_device.operation_fingerprint);
444 }
445
446 test "random philox family artifact carries runtime launch contract" {
447 const allocator = std.testing.allocator;
448 var state = gpu.recording.BackendState{
449 .allocator = allocator,
450 .kind = .cuda,
451 .format = .cuda_ptx,
452 };
453 const instance = Philox{ .count = 1024, .threads = 64 };
454
455 var family_artifact = try createPhiloxFamilyArtifact(allocator, state.handle(), instance, .{ .limits = .testing });
456 defer family_artifact.deinit();
457
458 const family_entry = family_artifact.entry();
459 try std.testing.expectEqualStrings("accy.kernel.random.philox_family_10r_64_f32", family_entry.target);
460 try std.testing.expectEqualStrings("accy_kernel_random_philox_family_10r_64_f32", family_entry.entry_name);
461 try std.testing.expectEqual(@as(u32, 4), family_entry.argument_count);
462 try std.testing.expectEqual(@as(u32, 3), family_entry.runtime_scalar_argument_count);
463 try std.testing.expect(family_entry.required_dtypes.contains(.f32));
464 try std.testing.expect(family_entry.shape_family_fingerprint != null);
465 const profile = family_entry.shape_profile orelse return error.TestExpectedShapeProfile;
466 try std.testing.expectEqualStrings("philox", profile.name);
467 try std.testing.expectEqual(@as(usize, 1), profile.dimensions.len);
468 switch (family_entry.launch) {
469 .derived => |launch| {
470 try std.testing.expectEqual(@as(u32, 64), launch.threadgroup[0]);
471 switch (launch.grid[0]) {
472 .runtime_u32_ceil_div => |term| {
473 try std.testing.expectEqual(@as(usize, 0), term.argument_index);
474 try std.testing.expectEqual(@as(u32, 256), term.divisor);
475 },
476 else => return error.TestExpectedDerivedLaunch,
477 }
478 },
479 else => return error.TestExpectedDerivedLaunch,
480 }
481 }
482
483 test "random philox key family artifacts carry key dtype contracts" {
484 const allocator = std.testing.allocator;
485 var state = gpu.recording.BackendState{
486 .allocator = allocator,
487 .kind = .cuda,
488 .format = .cuda_ptx,
489 };
490
491 var split_artifact = try createPhiloxKeySplitFamilyArtifact(
492 allocator,
493 state.handle(),
494 .{ .count = 1024, .threads = 64 },
495 .{ .limits = .testing },
496 );
497 defer split_artifact.deinit();
498 const split_entry = split_artifact.entry();
499 try std.testing.expectEqualStrings("accy.kernel.random.philox_key_split_family_10r_64_key", split_entry.target);
500 try std.testing.expectEqual(@as(u32, 3), split_entry.argument_count);
501 try std.testing.expectEqual(@as(u32, 1), split_entry.runtime_scalar_argument_count);
502 try std.testing.expect(split_entry.required_dtypes.contains(.key));
503
504 var uniform_artifact = try createPhiloxKeyUniformFamilyArtifact(
505 allocator,
506 state.handle(),
507 .{ .count = 1024, .threads = 64, .dtype = .f32 },
508 .{ .limits = .testing },
509 );
510 defer uniform_artifact.deinit();
511 const uniform_entry = uniform_artifact.entry();
512 try std.testing.expectEqualStrings("accy.kernel.random.philox_key_uniform_family_10r_64_f32", uniform_entry.target);
513 try std.testing.expectEqual(@as(u32, 3), uniform_entry.argument_count);
514 try std.testing.expectEqual(@as(u32, 1), uniform_entry.runtime_scalar_argument_count);
515 try std.testing.expect(uniform_entry.required_dtypes.contains(.key));
516 try std.testing.expect(uniform_entry.required_dtypes.contains(.f32));
517
518 var counter_artifact = try createPhiloxKeyCounterUniformFamilyArtifact(
519 allocator,
520 state.handle(),
521 .{ .count = 1024, .threads = 64, .dtype = .f32 },
522 .{ .limits = .testing },
523 );
524 defer counter_artifact.deinit();
525 const counter_entry = counter_artifact.entry();
526 try std.testing.expectEqualStrings("accy.kernel.random.philox_key_counter_uniform_family_10r_64_f32", counter_entry.target);
527 try std.testing.expectEqual(@as(u32, 4), counter_entry.argument_count);
528 try std.testing.expectEqual(@as(u32, 1), counter_entry.runtime_scalar_argument_count);
529 try std.testing.expect(counter_entry.required_dtypes.contains(.key));
530 try std.testing.expect(counter_entry.required_dtypes.contains(.i32));
531 try std.testing.expect(counter_entry.required_dtypes.contains(.f32));
532 }
533
534 test "random philox key counter uniform samples from scalar key and counter on CPU" {
535 const allocator = std.testing.allocator;
536 const compiled = PhiloxKeyCounterUniform{ .count = 1, .threads = 4 };
537 const runtime = PhiloxKeyCounterUniform{ .count = 6, .threads = 4 };
538 const seed = Key.init(0x01234567_89abcdef);
539 var counter = [_]i32{3};
540
541 var graph = try PhiloxKeyCounterUniformRuntimeFamilyF32.build(allocator, PhiloxKeyCounterUniformRuntimeFamilyF32.Limits.testing, compiled);
542 defer graph.deinit();
543
544 var dst = @as([8]f32, @splat(-1.0));
545 var keys = [_]Key{seed};
546
547 const launch_value = try entry.runtimeLaunch1D(runtime.count, runtime.threads);
548 try graph.runCpuWithLaunch(allocator, &.{
549 kernel.argumentBuffer(f32, dst[0..]),
550 kernel.argumentBuffer(Key, keys[0..]),
551 kernel.argumentBuffer(i32, counter[0..]),
552 kernel.argumentI32(@intCast(runtime.count)),
553 }, .{
554 .grid = launch_value.grid,
555 .block = launch_value.threadgroup,
556 });
557
558 for (0..runtime.count) |index| {
559 const words = philoxBlock(
560 philox_default_rounds,
561 .{ @intCast(index), @bitCast(counter[0]), 0, 0 },
562 .{ seed.lo(), seed.hi() },
563 );
564 try std.testing.expectApproxEqAbs(uniformFromBits(words[0]), dst[index], 0.0);
565 }
566 try std.testing.expectEqual(@as(f32, -1.0), dst[6]);
567 }
568
569 test "random philox fold family accumulates uniform samples on CPU" {
570 const allocator = std.testing.allocator;
571 const compiled = PhiloxFold{ .count = 1, .threads = 4 };
572 const runtime = PhiloxFold{ .count = 6, .samples = 5, .threads = 4, .seed = 0x00c0ffee };
573
574 var graph = try PhiloxFoldRuntimeFamilyF32.build(allocator, PhiloxFoldRuntimeFamilyF32.Limits.testing, compiled);
575 defer graph.deinit();
576
577 var dst = @as([8]f32, @splat(-1.0));
578
579 const launch_value = try entry.runtimeLaunch1D(runtime.count, runtime.threads);
580 try graph.runCpuWithLaunch(allocator, &.{
581 kernel.argumentBuffer(f32, dst[0..]),
582 kernel.argumentI32(@intCast(runtime.count)),
583 kernel.argumentI32(@intCast(runtime.samples)),
584 kernel.argumentI32(@bitCast(runtime.seedLo())),
585 kernel.argumentI32(@bitCast(runtime.seedHi())),
586 }, .{
587 .grid = launch_value.grid,
588 .block = launch_value.threadgroup,
589 });
590
591 for (0..6) |output| {
592 const expected = philoxFoldUniformReference(
593 philox_default_rounds,
594 @intCast(output),
595 runtime.samples,
596 .{ runtime.seedLo(), runtime.seedHi() },
597 );
598 try std.testing.expectEqual(expected, dst[output]);
599 try std.testing.expect(dst[output] >= 0.0);
600 try std.testing.expect(dst[output] < @as(f32, @floatFromInt(runtime.samples * philox_lanes)));
601 }
602 try std.testing.expectEqual(@as(f32, -1.0), dst[6]);
603 try std.testing.expectEqual(@as(f32, -1.0), dst[7]);
604 }
605
606 test "random threefry fold family xors raw words on CPU" {
607 const allocator = std.testing.allocator;
608 const compiled = ThreefryFold{ .count = 1, .dtype = .i32, .threads = 4 };
609 const runtime = ThreefryFold{ .count = 5, .samples = 7, .dtype = .i32, .threads = 4, .seed = 0xfeedface12345678 };
610
611 var graph = try ThreefryFoldRuntimeFamilyI32.build(allocator, ThreefryFoldRuntimeFamilyI32.Limits.testing, compiled);
612 defer graph.deinit();
613
614 var dst = @as([8]i32, @splat(0));
615
616 const launch_value = try entry.runtimeLaunch1D(runtime.count, runtime.threads);
617 try graph.runCpuWithLaunch(allocator, &.{
618 kernel.argumentBuffer(i32, dst[0..]),
619 kernel.argumentI32(@intCast(runtime.count)),
620 kernel.argumentI32(@intCast(runtime.samples)),
621 kernel.argumentI32(@bitCast(runtime.seedLo())),
622 kernel.argumentI32(@bitCast(runtime.seedHi())),
623 }, .{
624 .grid = launch_value.grid,
625 .block = launch_value.threadgroup,
626 });
627
628 for (0..5) |output| {
629 const expected = threefryFoldBitsReference(
630 threefry_default_rounds,
631 @intCast(output),
632 runtime.samples,
633 .{ runtime.seedLo(), runtime.seedHi() },
634 );
635 try std.testing.expectEqual(@as(i32, @bitCast(expected)), dst[output]);
636 }
637 }
638
639 test "random fold family artifact carries samples scalar in launch contract" {
640 const allocator = std.testing.allocator;
641 var state = gpu.recording.BackendState{
642 .allocator = allocator,
643 .kind = .cuda,
644 .format = .cuda_ptx,
645 };
646 const instance = PhiloxFold{ .count = 4096, .samples = 16, .rounds = 7, .threads = 128 };
647
648 var family_artifact = try createPhiloxFoldFamilyArtifact(allocator, state.handle(), instance, .{ .limits = .testing });
649 defer family_artifact.deinit();
650
651 const family_entry = family_artifact.entry();
652 try std.testing.expectEqualStrings("accy.kernel.random.philox_fold_family_7r_128_f32", family_entry.target);
653 try std.testing.expectEqual(@as(u32, 5), family_entry.argument_count);
654 try std.testing.expectEqual(@as(u32, 4), family_entry.runtime_scalar_argument_count);
655 const profile = family_entry.shape_profile orelse return error.TestExpectedShapeProfile;
656 try std.testing.expectEqual(@as(usize, 2), profile.dimensions.len);
657 try std.testing.expectEqual(@as(usize, 1), profile.dimensions[1].runtime_scalar_argument_index);
658 switch (family_entry.launch) {
659 .derived => |launch| {
660 try std.testing.expectEqual(@as(u32, 128), launch.threadgroup[0]);
661 switch (launch.grid[0]) {
662 .runtime_u32_ceil_div => |term| {
663 try std.testing.expectEqual(@as(usize, 0), term.argument_index);
664 try std.testing.expectEqual(@as(u32, 128), term.divisor);
665 },
666 else => return error.TestExpectedDerivedLaunch,
667 }
668 },
669 else => return error.TestExpectedDerivedLaunch,
670 }
671
672 const arguments = try philoxFoldRuntimeArguments(instance);
673 try std.testing.expectEqual(@as(u32, 4096), arguments[0].u32);
674 try std.testing.expectEqual(@as(u32, 16), arguments[1].u32);
675 }
676
677 test "random fold family tuning keys separate fills from folds" {
678 const allocator = std.testing.allocator;
679 const caps = randomFamilyTuningTestCapabilities(0x2684);
680 const device = tuning.deviceFingerprint(caps);
681
682 const fill_key = try philoxFamilyTuningKey(allocator, device, .{ .count = 4096 });
683 const fold_key = try philoxFoldFamilyTuningKey(allocator, device, .{ .count = 4096, .samples = 1 });
684 try std.testing.expect(!fill_key.eql(fold_key));
685 try std.testing.expect(fill_key.operation_fingerprint != fold_key.operation_fingerprint);
686
687 const more_samples = try philoxFoldFamilyTuningKey(allocator, device, .{ .count = 4096, .samples = 8 });
688 try std.testing.expect(!fold_key.eql(more_samples));
689 try std.testing.expectEqual(fold_key.family_fingerprint, more_samples.family_fingerprint);
690 try std.testing.expectEqual(fold_key.operation_fingerprint, more_samples.operation_fingerprint);
691
692 const fewer_rounds = try philoxFoldFamilyTuningKey(allocator, device, .{ .count = 4096, .samples = 1, .rounds = 7 });
693 try std.testing.expect(fold_key.operation_fingerprint != fewer_rounds.operation_fingerprint);
694
695 const threefry_key = try threefryFoldFamilyTuningKey(allocator, device, .{ .count = 4096 });
696 const squares_key = try squaresFoldFamilyTuningKey(allocator, device, .{ .count = 4096 });
697 try std.testing.expect(!threefry_key.eql(squares_key));
698
699 const records = [_]tuning.FamilyTuningRecord{.{
700 .key = fold_key,
701 .target = "accy.kernel.random.philox_fold_family_256_f32",
702 .winner_median_ns = 700,
703 .runner_up_median_ns = 1000,
704 .sample_count = 30,
705 }};
706 const reader = tuning.FamilyTuningReader.init(caps, .{ .records = records[0..] });
707 const hit_key = try philoxFoldFamilyTuningKey(allocator, reader.device_fingerprint, .{ .count = 4096, .samples = 1 });
708 const hit = reader.table.find(hit_key) orelse return error.TestExpectedTuningRecord;
709 try std.testing.expectEqualStrings("accy.kernel.random.philox_fold_family_256_f32", hit.target);
710 try std.testing.expect(reader.table.find(fill_key) == null);
711 }
712
713 test "random squares reference matches computed anchors for the documented key" {
714 try std.testing.expectEqual(@as(u32, 0x58db6965), squaresBlock(0, squares_default_key));
715 try std.testing.expectEqual(@as(u32, 0x3b63969b), squaresBlock(1, squares_default_key));
716 try std.testing.expectEqual(@as(u32, 0xbcd3723e), squaresBlock(2, squares_default_key));
717 try std.testing.expectEqual(@as(u32, 0xe617d09b), squaresBlock(0xffffffffffffffff, squares_default_key));
718 }
719
720 test "random squares runtime family emits raw bits with tail guard on CPU" {
721 const allocator = std.testing.allocator;
722 const compiled = Squares{ .count = 1, .dtype = .i32, .threads = 4 };
723 const runtime = Squares{ .count = 6, .dtype = .i32, .threads = 4 };
724
725 var graph = try SquaresRuntimeFamilyI32.build(allocator, SquaresRuntimeFamilyI32.Limits.testing, compiled);
726 defer graph.deinit();
727
728 var dst = @as([8]i32, @splat(0));
729
730 const launch_value = try entry.runtimeLaunch1D(runtime.count, runtime.threads);
731 try graph.runCpuWithLaunch(allocator, &.{
732 kernel.argumentBuffer(i32, dst[0..]),
733 kernel.argumentI32(@intCast(runtime.count)),
734 kernel.argumentI32(@bitCast(runtime.keyLo())),
735 kernel.argumentI32(@bitCast(runtime.keyHi())),
736 }, .{
737 .grid = launch_value.grid,
738 .block = launch_value.threadgroup,
739 });
740
741 for (0..6) |element| {
742 const expected = squaresBlock(@intCast(element), runtime.key);
743 try std.testing.expectEqual(@as(i32, @bitCast(expected)), dst[element]);
744 }
745 try std.testing.expectEqual(@as(i32, 0), dst[6]);
746 try std.testing.expectEqual(@as(i32, 0), dst[7]);
747 }
748
749 test "random squares fold family accumulates uniform samples on CPU" {
750 const allocator = std.testing.allocator;
751 const compiled = SquaresFold{ .count = 1, .threads = 4 };
752 const runtime = SquaresFold{ .count = 5, .samples = 6, .threads = 4 };
753
754 var graph = try SquaresFoldRuntimeFamilyF32.build(allocator, SquaresFoldRuntimeFamilyF32.Limits.testing, compiled);
755 defer graph.deinit();
756
757 var dst = @as([8]f32, @splat(-1.0));
758
759 const launch_value = try entry.runtimeLaunch1D(runtime.count, runtime.threads);
760 try graph.runCpuWithLaunch(allocator, &.{
761 kernel.argumentBuffer(f32, dst[0..]),
762 kernel.argumentI32(@intCast(runtime.count)),
763 kernel.argumentI32(@intCast(runtime.samples)),
764 kernel.argumentI32(@bitCast(runtime.keyLo())),
765 kernel.argumentI32(@bitCast(runtime.keyHi())),
766 }, .{
767 .grid = launch_value.grid,
768 .block = launch_value.threadgroup,
769 });
770
771 for (0..5) |output| {
772 const expected = squaresFoldUniformReference(@intCast(output), runtime.samples, runtime.key);
773 try std.testing.expectEqual(expected, dst[output]);
774 }
775 try std.testing.expectEqual(@as(f32, -1.0), dst[5]);
776 }
777
778 test "random squares family artifact carries runtime launch contract" {
779 const allocator = std.testing.allocator;
780 var state = gpu.recording.BackendState{
781 .allocator = allocator,
782 .kind = .cuda,
783 .format = .cuda_ptx,
784 };
785 const instance = Squares{ .count = 4096, .threads = 128 };
786
787 var family_artifact = try createSquaresFamilyArtifact(allocator, state.handle(), instance, .{ .limits = .testing });
788 defer family_artifact.deinit();
789
790 const family_entry = family_artifact.entry();
791 try std.testing.expectEqualStrings("accy.kernel.random.squares_family_128_f32", family_entry.target);
792 try std.testing.expectEqual(@as(u32, 4), family_entry.argument_count);
793 try std.testing.expectEqual(@as(u32, 3), family_entry.runtime_scalar_argument_count);
794
795 var owned = try squaresFamilySpecialization(allocator, instance);
796 defer owned.deinit();
797 const recovered = squaresInstanceFromSpecialization(owned.value) orelse return error.TestExpectedSquaresInstance;
798 try std.testing.expectEqual(instance.count, recovered.count);
799 try std.testing.expectEqual(instance.threads, recovered.threads);
800 try std.testing.expectEqual(@as(?Philox, null), philoxInstanceFromSpecialization(owned.value));
801 }
802
803 test "random reference streams keep every bit position near balance" {
804 const words_per_algorithm: u32 = 1 << 18;
805 var ones = @as([32]u64, @splat(0));
806
807 var counter: u32 = 0;
808 var produced: u32 = 0;
809 while (produced < words_per_algorithm) : (counter += 1) {
810 const words = philoxBlock(philox_default_rounds, .{ counter, 0, 0, 0 }, .{ 0x9e3779b9, 0x7f4a7c15 });
811 for (words) |word| {
812 for (0..32) |bit| ones[bit] += (word >> @intCast(bit)) & 1;
813 }
814 produced += philox_lanes;
815 }
816 try expectBitBalance(&ones, produced);
817
818 ones = @as([32]u64, @splat(0));
819 counter = 0;
820 produced = 0;
821 while (produced < words_per_algorithm) : (counter += 1) {
822 const words = threefryBlock(threefry_default_rounds, .{ counter, 0 }, .{ 0x243f6a88, 0x85a308d3 });
823 for (words) |word| {
824 for (0..32) |bit| ones[bit] += (word >> @intCast(bit)) & 1;
825 }
826 produced += threefry_lanes;
827 }
828 try expectBitBalance(&ones, produced);
829
830 ones = @as([32]u64, @splat(0));
831 counter = 0;
832 while (counter < words_per_algorithm) : (counter += 1) {
833 const word = squaresBlock(counter, squares_default_key);
834 for (0..32) |bit| ones[bit] += (word >> @intCast(bit)) & 1;
835 }
836 try expectBitBalance(&ones, words_per_algorithm);
837 }
838
839 fn expectBitBalance(ones: *const [32]u64, words: u32) !void {
840 const half: f64 = @as(f64, @floatFromInt(words)) / 2.0;
841 const tolerance = half * 0.02;
842 for (ones) |count| {
843 const deviation = @abs(@as(f64, @floatFromInt(count)) - half);
844 try std.testing.expect(deviation < tolerance);
845 }
846 }
847
848 test "random instances round-trip through specializations" {
849 const philox_instance = Philox{ .count = 4096, .rounds = 7, .dtype = .i32, .threads = 128 };
850 var philox_owned = try philoxFamilySpecialization(std.testing.allocator, philox_instance);
851 defer philox_owned.deinit();
852
853 const philox_recovered = philoxInstanceFromSpecialization(philox_owned.value) orelse return error.TestExpectedPhiloxInstance;
854 try std.testing.expectEqual(philox_instance.count, philox_recovered.count);
855 try std.testing.expectEqual(philox_instance.rounds, philox_recovered.rounds);
856 try std.testing.expectEqual(philox_instance.dtype, philox_recovered.dtype);
857 try std.testing.expectEqual(philox_instance.threads, philox_recovered.threads);
858 try std.testing.expectEqual(@as(?Threefry, null), threefryInstanceFromSpecialization(philox_owned.value));
859
860 const threefry_instance = Threefry{ .count = 1000, .rounds = 13, .threads = 64 };
861 var threefry_owned = try threefryFamilySpecialization(std.testing.allocator, threefry_instance);
862 defer threefry_owned.deinit();
863
864 const threefry_recovered = threefryInstanceFromSpecialization(threefry_owned.value) orelse return error.TestExpectedThreefryInstance;
865 try std.testing.expectEqual(threefry_instance.count, threefry_recovered.count);
866 try std.testing.expectEqual(threefry_instance.rounds, threefry_recovered.rounds);
867 try std.testing.expectEqual(threefry_instance.dtype, threefry_recovered.dtype);
868 try std.testing.expectEqual(threefry_instance.threads, threefry_recovered.threads);
869 try std.testing.expectEqual(@as(?Philox, null), philoxInstanceFromSpecialization(threefry_owned.value));
870
871 try std.testing.expectEqual(@as(?Philox, null), philoxInstanceFromSpecialization(.{}));
872 }
873
874 test "accy kernel library random declaration coverage" {
875 std.testing.refAllDecls(random);
876 std.testing.refAllDecls(@This());
877 }