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 }