lib/accy/src/profiling/versus/dump.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gpu = @import("gpu");
3 const accy = @import("accy");
4 const bench = @import("bench");
5 const sys = @import("sys");
6
7 const runner = @import("runner.zig");
8 const shade = @import("shade.zig");
9 const workload_mod = @import("workload.zig");
10
11 const Allocator = std.mem.Allocator;
12 const Workload = workload_mod.Workload;
13 const pretty_json = bench.pretty.json;
14
15 pub const Options = struct {
16 workload: []const u8 = "",
17 dir: []const u8 = "",
18 frames: u32 = 1,
19 dt: f32 = 2.0e-4,
20 math_tier: gpu.BackendMathTier = .exact,
21 };
22
23 pub const substeps = 8;
24 pub const disk_radius: f32 = 0.9;
25 pub const disk_thickness: f32 = 0.05;
26 pub const central_mass: f32 = 50.0;
27 pub const body_mass: f32 = 0.002;
28
29 fn compileAndLoadModule(
30 allocator: Allocator,
31 handle: gpu.BackendHandle,
32 module: *accy.choir.SemanticModule,
33 options: accy.executable.FragmentCompilerOptions,
34 ) !*accy.executable.LoadedFragment {
35 const compiled = try accy.executable.compileFragmentFromSemanticModule(allocator, handle, module, options);
36 return try accy.executable.loadFragment(allocator, handle, compiled, options);
37 }
38
39 pub fn run(arena: Allocator, backing: Allocator, out: *std.Io.Writer, options: Options) !u8 {
40 const workload = workload_mod.byName(options.workload) orelse return error.InvalidArguments;
41
42 var state = gpu.cuda.State.initDevice(backing, 0) catch |err| switch (err) {
43 error.RuntimeUnavailable => {
44 try out.print("dump: CUDA driver or device unavailable\n", .{});
45 try out.flush();
46 return 3;
47 },
48 else => return err,
49 };
50 defer state.deinit();
51
52 const module = try runner.buildModule(backing, workload, workload_mod.default_scene);
53 var fragment = try compileAndLoadModule(backing, state.handle(), module, .{ .math_tier = options.math_tier });
54 defer fragment.deinit();
55
56 var buffers = try runner.allocHostBuffers(backing, workload);
57 defer buffers.deinit(backing);
58
59 try sys.fs.createDirPath(options.dir);
60
61 const frames: u32 = switch (workload.kind) {
62 .nbody, .mandelbrot, .raymarch => options.frames,
63 else => 1,
64 };
65
66 switch (workload.kind) {
67 .nbody => try dumpNbody(arena, backing, fragment, workload, &buffers, options),
68 .mandelbrot, .raymarch => try dumpScenes(arena, backing, &state, workload, &buffers, options),
69 else => {
70 var input_storage: [4][]const u8 = undefined;
71 const inputs = runner.inputBytes(&buffers, &input_storage);
72 const bindings = try accy.executable.prepareInvocation(fragment, backing, inputs);
73 defer bindings.deinit();
74 try bindings.launch(backing);
75 try bindings.readOutput(0, std.mem.sliceAsBytes(buffers.output));
76 try writeFrame(arena, options.dir, workload.name, 0, buffers.output);
77 },
78 }
79
80 try writeMeta(arena, options.dir, workload, frames, options.dt);
81 try out.print("dump: {s} frames={d} dir={s}\n", .{ workload.name, frames, options.dir });
82 try out.flush();
83 return 0;
84 }
85
86 pub fn seedDisk(px: []f32, py: []f32, pz: []f32, mass: []f32, vel: []f32) void {
87 const bodies = px.len;
88 px[0] = 0;
89 py[0] = 0;
90 pz[0] = 0;
91 mass[0] = central_mass;
92 vel[0] = 0;
93 vel[bodies] = 0;
94 vel[2 * bodies] = 0;
95
96 var index: usize = 1;
97 while (index < bodies) : (index += 1) {
98 const u = @as(f32, @floatFromInt(index)) / @as(f32, @floatFromInt(bodies));
99 const radius = disk_radius * @sqrt(0.02 + 0.98 * u);
100 const angle = @as(f32, @floatFromInt(index)) * 2.399963;
101 px[index] = radius * @cos(angle);
102 pz[index] = radius * @sin(angle);
103 py[index] = disk_thickness * workload_mod.fillValue(index);
104 mass[index] = body_mass * (1.0 + workload_mod.fillValue(index * 7 + 3));
105
106 const speed = @sqrt(central_mass / radius);
107 vel[index] = -speed * @sin(angle);
108 vel[bodies + index] = 0;
109 vel[2 * bodies + index] = speed * @cos(angle);
110 }
111 }
112
113 pub fn integrate(px: []f32, py: []f32, pz: []f32, vel: []f32, accels: []const f32, dt: f32) void {
114 const bodies = px.len;
115 for (0..bodies) |body| {
116 vel[body] += accels[body] * dt;
117 vel[bodies + body] += accels[bodies + body] * dt;
118 vel[2 * bodies + body] += accels[2 * bodies + body] * dt;
119 px[body] += vel[body] * dt;
120 py[body] += vel[bodies + body] * dt;
121 pz[body] += vel[2 * bodies + body] * dt;
122 }
123 }
124
125 pub fn sceneForFrame(kind: workload_mod.Kind, frame: u32, frames: u32) workload_mod.Scene {
126 var scene = workload_mod.default_scene;
127 if (frame == 0 or frames <= 1) return scene;
128 const t = @as(f32, @floatFromInt(frame)) / @as(f32, @floatFromInt(frames));
129 switch (kind) {
130 .mandelbrot => {
131 const center_x: f32 = -0.74364388;
132 const center_y: f32 = 0.13182590;
133 const span = workload_mod.mandelbrot_span * std.math.pow(f32, 0.962, @floatFromInt(frame));
134 scene.mandelbrot_span = span;
135 scene.mandelbrot_x0 = center_x - span * 0.5;
136 scene.mandelbrot_y0 = center_y - span * 0.5;
137 },
138 .raymarch => {
139 const tau = 2.0 * std.math.pi;
140 const phases = [3]f32{ 0.0, 2.1, 4.2 };
141 const amps = [3]f32{ 0.55, 0.35, 0.45 };
142 for (&scene.spheres, phases, amps) |*sphere, phase, amp| {
143 sphere[1] += amp * @sin(tau * t + phase);
144 sphere[0] += 0.3 * amp * @sin(2.0 * tau * t + phase);
145 }
146 },
147 else => {},
148 }
149 return scene;
150 }
151
152 fn dumpScenes(
153 arena: Allocator,
154 backing: Allocator,
155 state: *gpu.cuda.State,
156 workload: Workload,
157 buffers: *runner.HostBuffers,
158 options: Options,
159 ) !void {
160 var frame: u32 = 0;
161 while (frame < options.frames) : (frame += 1) {
162 const scene = sceneForFrame(workload.kind, frame, options.frames);
163 const module = switch (workload.kind) {
164 .raymarch => try shade.buildShadedModule(backing, workload, scene),
165 else => try runner.buildModule(backing, workload, scene),
166 };
167 var preparation_failure: accy.preparation.BackendPreparationFailure = .{};
168 defer preparation_failure.deinit(backing);
169 var fragment = compileAndLoadModule(backing, state.handle(), module, .{
170 .math_tier = options.math_tier,
171 .preparation_failure = &preparation_failure,
172 }) catch |err| {
173 if (err == error.PassFailed) try writePreparationFailure(arena, workload, &preparation_failure);
174 return err;
175 };
176 defer fragment.deinit();
177 var input_storage: [4][]const u8 = undefined;
178 const inputs = runner.inputBytes(buffers, &input_storage);
179 const bindings = try accy.executable.prepareInvocation(fragment, backing, inputs);
180 defer bindings.deinit();
181 try bindings.launch(backing);
182 try bindings.readOutput(0, std.mem.sliceAsBytes(buffers.output));
183 if (workload.kind == .mandelbrot) {
184 try runner.verifyMandelbrotFrame(workload, scene, buffers.output);
185 }
186 try writeFrame(arena, options.dir, workload.name, frame, buffers.output);
187 }
188 }
189
190 fn dumpNbody(
191 arena: Allocator,
192 backing: Allocator,
193 fragment: *accy.executable.LoadedFragment,
194 workload: Workload,
195 buffers: *runner.HostBuffers,
196 options: Options,
197 ) !void {
198 const bodies: usize = workload.m;
199 const vel = try backing.alloc(f32, 3 * bodies);
200 defer backing.free(vel);
201
202 seedDisk(buffers.inputs[0], buffers.inputs[1], buffers.inputs[2], buffers.inputs[3], vel);
203
204 const positions = try backing.alloc(f32, 3 * bodies);
205 defer backing.free(positions);
206
207 var frame: u32 = 0;
208 while (frame < options.frames) : (frame += 1) {
209 @memcpy(positions[0..bodies], buffers.inputs[0]);
210 @memcpy(positions[bodies .. 2 * bodies], buffers.inputs[1]);
211 @memcpy(positions[2 * bodies ..], buffers.inputs[2]);
212 try writeFrame(arena, options.dir, workload.name, frame, positions);
213
214 var step: u32 = 0;
215 while (step < substeps) : (step += 1) {
216 var input_storage: [4][]const u8 = undefined;
217 const inputs = runner.inputBytes(buffers, &input_storage);
218 const bindings = try accy.executable.prepareInvocation(fragment, backing, inputs);
219 defer bindings.deinit();
220 try bindings.launch(backing);
221 try bindings.readOutput(0, std.mem.sliceAsBytes(buffers.output));
222 try runner.verifyNbodyAccels(workload, buffers);
223 integrate(buffers.inputs[0], buffers.inputs[1], buffers.inputs[2], vel, buffers.output, options.dt);
224 }
225 }
226 }
227
228 fn writePreparationFailure(
229 arena: Allocator,
230 workload: Workload,
231 failure: *const accy.preparation.BackendPreparationFailure,
232 ) !void {
233 var report = try bench.pretty.diagnostic.Report.init(arena, "versus dump: kernel preparation failed");
234 defer report.deinit();
235 try report.field("workload", "{s}", .{workload.name});
236 if (failure.pipeline_name) |name| try report.field("pipeline", "{s}", .{name});
237 if (failure.pass_name) |name| try report.field("pass", "{s}", .{name});
238 if (failure.target_op_name) |name| try report.field("op", "{s}", .{name});
239 if (failure.target_symbol_name) |name| try report.field("symbol", "{s}", .{name});
240 if (failure.verifier_error_name) |name| try report.field("verifier_error", "{s}", .{name});
241 bench.pretty.diagnostic.writeStderr(&report, .{});
242 }
243
244 fn writeFrame(arena: Allocator, dir: []const u8, name: []const u8, frame: u32, values: []const f32) !void {
245 const path = try std.fmt.allocPrint(arena, "{s}/{s}-f{d:0>4}.f32", .{ dir, name, frame });
246 defer arena.free(path);
247 try sys.fs.writeFile(path, std.mem.sliceAsBytes(values));
248 }
249
250 fn writeMeta(arena: Allocator, dir: []const u8, workload: Workload, frames: u32, dt: f32) !void {
251 const path = try std.fmt.allocPrint(arena, "{s}/{s}.json", .{ dir, workload.name });
252 defer arena.free(path);
253 var buffer: [512]u8 = undefined;
254 var meta = std.Io.Writer.fixed(&buffer);
255 var stream = pretty_json.Writer.init(&meta, .minified);
256 const object = try stream.object();
257 try object.field("workload", workload.name);
258 try object.field("kind", workload.kind.name());
259 try object.field("m", workload.m);
260 try object.field("n", workload.n);
261 try object.field("frames", frames);
262 try object.print("dt", "{e:.3}", .{dt});
263 try object.field("substeps", substeps);
264 try object.field("layout", switch (workload.kind) {
265 .nbody => "px,py,pz",
266 .raymarch => "row-major shade, sky zero",
267 else => "row-major",
268 });
269 try object.endLine();
270 try sys.fs.writeFile(path, meta.buffered());
271 }
272
273 test "seedDisk places a heavy center and orbital shells" {
274 var px: [64]f32 = undefined;
275 var py: [64]f32 = undefined;
276 var pz: [64]f32 = undefined;
277 var mass: [64]f32 = undefined;
278 var vel: [192]f32 = undefined;
279 seedDisk(px[0..], py[0..], pz[0..], mass[0..], vel[0..]);
280
281 try std.testing.expectEqual(central_mass, mass[0]);
282 for (1..64) |body| {
283 const radius = @sqrt(px[body] * px[body] + pz[body] * pz[body]);
284 try std.testing.expect(radius > 0.05 and radius <= disk_radius + 1e-4);
285 try std.testing.expect(mass[body] > 0);
286 const speed = @sqrt(vel[body] * vel[body] + vel[128 + body] * vel[128 + body]);
287 try std.testing.expectApproxEqRel(@sqrt(central_mass / radius), speed, 1e-4);
288 }
289 }
290
291 test "verifyNbodyAccels accepts oracle accels for the disk and rejects corruption" {
292 var workload = workload_mod.byName("nbody_f32_4096") orelse return error.TestUnexpectedResult;
293 workload.m = 64;
294 const bodies: usize = workload.m;
295
296 var buffers = try runner.allocHostBuffers(std.testing.allocator, workload);
297 defer buffers.deinit(std.testing.allocator);
298 const vel = try std.testing.allocator.alloc(f32, 3 * bodies);
299 defer std.testing.allocator.free(vel);
300 seedDisk(buffers.inputs[0], buffers.inputs[1], buffers.inputs[2], buffers.inputs[3], vel);
301
302 const px = buffers.inputs[0];
303 const py = buffers.inputs[1];
304 const pz = buffers.inputs[2];
305 const mass = buffers.inputs[3];
306 for (0..3 * bodies) |flat| {
307 const axis = flat / bodies;
308 const body = flat % bodies;
309 var acc: f64 = 0;
310 for (0..bodies) |other| {
311 const dx = @as(f64, px[other]) - @as(f64, px[body]);
312 const dy = @as(f64, py[other]) - @as(f64, py[body]);
313 const dz = @as(f64, pz[other]) - @as(f64, pz[body]);
314 const r2 = dx * dx + dy * dy + dz * dz + workload_mod.nbody_softening;
315 const weight = @as(f64, mass[other]) / (r2 * @sqrt(r2));
316 acc += weight * switch (axis) {
317 0 => dx,
318 1 => dy,
319 else => dz,
320 };
321 }
322 buffers.output[flat] = @floatCast(acc);
323 }
324 try runner.verifyNbodyAccels(workload, &buffers);
325
326 buffers.output[0] += 1000.0;
327 try std.testing.expectError(error.OracleMismatch, runner.verifyNbodyAccels(workload, &buffers));
328 }
329
330 test "integrate holds a circular two-body orbit near its radius" {
331 var px = [_]f32{ 0, 1 };
332 var py = [_]f32{ 0, 0 };
333 var pz = [_]f32{ 0, 0 };
334 var vel = [_]f32{ 0, 0, 0, 0, 0, @sqrt(central_mass) };
335 var accels: [6]f32 = undefined;
336
337 var step: usize = 0;
338 while (step < 4000) : (step += 1) {
339 for (0..2) |body| {
340 var ax: f64 = 0;
341 var ay: f64 = 0;
342 var az: f64 = 0;
343 const masses = [_]f32{ central_mass, 0.0 };
344 for (0..2) |other| {
345 const dx = @as(f64, px[other]) - @as(f64, px[body]);
346 const dy = @as(f64, py[other]) - @as(f64, py[body]);
347 const dz = @as(f64, pz[other]) - @as(f64, pz[body]);
348 const r2 = dx * dx + dy * dy + dz * dz + workload_mod.nbody_softening;
349 const weight = @as(f64, masses[other]) / (r2 * @sqrt(r2));
350 ax += dx * weight;
351 ay += dy * weight;
352 az += dz * weight;
353 }
354 accels[body] = @floatCast(ax);
355 accels[2 + body] = @floatCast(ay);
356 accels[4 + body] = @floatCast(az);
357 }
358 integrate(px[0..], py[0..], pz[0..], vel[0..], accels[0..], 1.0e-4);
359 }
360
361 const radius = @sqrt(px[1] * px[1] + py[1] * py[1] + pz[1] * pz[1]);
362 try std.testing.expect(radius > 0.9 and radius < 1.1);
363 }