diff --git a/zig/build/cuda.zig b/zig/build/cuda.zig index 684d6c3cc9..0c97f92dbf 100644 --- a/zig/build/cuda.zig +++ b/zig/build/cuda.zig @@ -240,6 +240,8 @@ pub fn hostTests(b: *std.Build, draft_ids: *std.Build.Module, build_options: *st step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("zig/build/check_fatbins.zig"), .target = host }) })).step); const cuda = runtime(b, host, .debug, &.{}); const mods = family(b, host, .debug, cuda, draft_ids); + const contract_arch = b.createModule(.{ .root_source_file = b.path("zig/tests/cuda/contract_arch_test.zig"), .target = host, .optimize = .debug, .link_libc = true, .imports = &.{ .{ .name = "cuda", .module = cuda }, .{ .name = "core", .module = mods.core } } }); + step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = contract_arch })).step); const native_modules = engines(b, host, .debug, cuda, mods.core, mods.lanes, mods.nemotron, mods.heat); const native = native_modules.engines; // the HTTP server's unit tests (zig/src/server/root.zig), which the macOS build runs, on Linux hosts too diff --git a/zig/tests/cuda/contract_arch_test.zig b/zig/tests/cuda/contract_arch_test.zig new file mode 100644 index 0000000000..d355c8d8ba --- /dev/null +++ b/zig/tests/cuda/contract_arch_test.zig @@ -0,0 +1,123 @@ +//! Host-only checks of the CUDA contract's architecture selection and its real gate-3 routing. +const std = @import("std"); +const mm = @import("contract_mm.zig"); +const contract = @import("core").contract; +const g = contract.gates; + +test "contract architecture: older SMs omit only the standalone cluster kernel" { + for ([_]u32{ 80, 86, 89 }) |sm| { + var count: usize = 0; + for (std.enums.values(mm.Kind)) |kind| { + var m: mm.Mm = .{ .rig = undefined, .kind = kind, .sm = sm }; + const k = m.kernel(); + try std.testing.expectEqual(kind == .qmm_group, k == null); + if (k != null) count += 1; + } + try std.testing.expectEqual(@as(usize, 6), count); + } +} + +test "contract architecture: SM90 and Blackwell retain every kernel and the GEMV bit comparisons" { + for ([_]u32{ 90, 120, 121 }) |sm| { + var count: usize = 0; + for (std.enums.values(mm.Kind)) |kind| { + var m: mm.Mm = .{ .rig = undefined, .kind = kind, .sm = sm }; + const k = m.kernel().?; + count += 1; + try std.testing.expectEqual(kind == .lane_gemv or kind == .lane_gemv_split, k.spec.replaces != null); + } + try std.testing.expectEqual(@as(usize, 7), count); + } +} + +test "contract architecture: legacy GEMV keeps every field except the unavailable replacement" { + for ([_]u32{ 80, 86, 89 }) |sm| for ([_]mm.Kind{ .lane_gemv, .lane_gemv_split }) |kind| { + var modern: mm.Mm = .{ .rig = undefined, .kind = kind, .sm = 121 }; + var legacy: mm.Mm = .{ .rig = undefined, .kind = kind, .sm = sm }; + var want = modern.kernel().?.spec; + want.replaces = null; + try std.testing.expectEqualDeep(want, legacy.kernel().?.spec); + }; +} + +test "contract architecture: the legacy FP4 lane names the reduction fallback it actually selects" { + for ([_]u32{ 80, 86, 89 }) |sm| { + var m: mm.Mm = .{ .rig = undefined, .kind = .lane_f32, .sm = sm }; + const k = m.kernel().?; + try std.testing.expect(std.mem.indexOf(u8, k.spec.name, "reduce_kernel") != null); + } +} + +const Cpu = struct { + bytes: [2]u8 = .{ 0x80, 0x3f }, // bf16 1 + baseline: [2]u8 = .{ 0x80, 0x3f }, + references: usize = 0, + replacements: usize = 0, + const vt: g.VTable = .{ .prepare = prepare, .run = run, .output = output, .inputs = inputs, .intact = intact, .overlap = overlap, .count = count, .reference = reference, .replaced = replaced }; + + fn self(ctx: *anyopaque) *Cpu { + return @ptrCast(@alignCast(ctx)); + } + fn prepare(_: *anyopaque, _: contract.spec.Shape, _: g.Fill, _: u64) anyerror!bool { + return true; + } + fn run(_: *anyopaque) anyerror!void {} + fn output(ctx: *anyopaque) anyerror![]const u8 { + return &self(ctx).bytes; + } + fn inputs(_: *anyopaque) anyerror![]const u8 { + return &.{}; + } + fn intact(_: *anyopaque) anyerror!bool { + return true; + } + fn overlap(_: *anyopaque) bool { + return false; + } + fn count(_: *anyopaque) usize { + return 1; + } + fn reference(ctx: *anyopaque, want: []f64, mag: []f64) anyerror!void { + self(ctx).references += 1; + want[0] = 1; + mag[0] = 1; + } + fn replaced(ctx: *anyopaque) anyerror![]const u8 { + self(ctx).replacements += 1; + return &self(ctx).baseline; + } +}; + +test "contract architecture: legacy gate 3 really checks FP64 and rejects a planted bad output" { + const old = g.quiet; + g.quiet = true; + defer g.quiet = old; + for ([_]mm.Kind{ .lane_gemv, .lane_gemv_split }) |kind| { + var m: mm.Mm = .{ .rig = undefined, .kind = kind, .sm = 89 }; + var cpu: Cpu = .{}; + const k: g.Kernel = .{ .spec = m.kernel().?.spec, .ctx = &cpu, .vt = &Cpu.vt }; + try std.testing.expectEqual(g.Verdict.pass, try g.shapes(std.testing.allocator, k, 1)); + try std.testing.expect(cpu.references > 0); + try std.testing.expectEqual(@as(usize, 0), cpu.replacements); + cpu.bytes = .{ 0, 0 }; + try std.testing.expectEqual(g.Verdict.fail, try g.shapes(std.testing.allocator, k, 1)); + try std.testing.expectEqual(@as(usize, 0), cpu.replacements); + } +} + +test "contract architecture: modern gate 3 retains bit equality and rejects a planted baseline difference" { + const old = g.quiet; + g.quiet = true; + defer g.quiet = old; + for ([_]mm.Kind{ .lane_gemv, .lane_gemv_split }) |kind| { + var m: mm.Mm = .{ .rig = undefined, .kind = kind, .sm = 121 }; + var cpu: Cpu = .{}; + const k: g.Kernel = .{ .spec = m.kernel().?.spec, .ctx = &cpu, .vt = &Cpu.vt }; + try std.testing.expectEqual(g.Verdict.pass, try g.shapes(std.testing.allocator, k, 1)); + try std.testing.expect(cpu.replacements > 0); + try std.testing.expectEqual(@as(usize, 0), cpu.references); + cpu.baseline = .{ 0, 0 }; + try std.testing.expectEqual(g.Verdict.fail, try g.shapes(std.testing.allocator, k, 1)); + try std.testing.expectEqual(@as(usize, 0), cpu.references); + } +} diff --git a/zig/tests/cuda/contract_mm.zig b/zig/tests/cuda/contract_mm.zig index fb97d89818..89773961d5 100644 --- a/zig/tests/cuda/contract_mm.zig +++ b/zig/tests/cuda/contract_mm.zig @@ -55,6 +55,7 @@ pub fn spec(kind: Kind) c.Spec { pub const Mm = struct { rig: rig_.Rig, kind: Kind, + sm: u32, affine4: ?*const qlinear.Affine4Kernels = null, split: ?*const qlinear.Split = null, prompt: ?*const nvfp4.Prompt = null, @@ -75,8 +76,14 @@ pub const Mm = struct { self.rig.deinit(); } - pub fn kernel(self: *Mm) g.Kernel { - return .{ .spec = spec(self.kind), .ctx = self, .vt = &vt }; + pub fn kernel(self: *Mm) ?g.Kernel { + if (self.sm < 90 and self.kind == .qmm_group) return null; + var declared = spec(self.kind); + if (self.sm < 90) { + declared.replaces = null; + if (self.kind == .lane_f32) declared.name = "cuda qmmf FP4 fp32 out (slices by reduce_kernel, architecture fallback)"; + } + return .{ .spec = declared, .ctx = self, .vt = &vt }; } const vt = g.hooks(Mm); diff --git a/zig/tests/cuda/contract_tests.zig b/zig/tests/cuda/contract_tests.zig index e87a2e90e5..da217fb961 100644 --- a/zig/tests/cuda/contract_tests.zig +++ b/zig/tests/cuda/contract_tests.zig @@ -82,10 +82,18 @@ pub fn run(gpu: check.Gpu) !void { .lane_f32_reduce => &lane_reduce, else => null, }; - m.* = .{ .rig = .{ .gpu = gpu, .s = s }, .kind = kind, .affine4 = &affine4, .split = &split, .prompt = &prompt, .lane = by_lane }; + m.* = .{ .rig = .{ .gpu = gpu, .s = s }, .kind = kind, .sm = cap, .affine4 = &affine4, .split = &split, .prompt = &prompt, .lane = by_lane }; } defer for (&mm) |*m| m.deinit(); - for (&mm) |*m| try kernels.append(a, m.kernel()); + for (&mm) |*m| { + const k = m.kernel() orelse { + std.debug.print("N/A cuda qmm_group (cluster tile): needs sm_90 or newer, this GPU is sm_{d}\n", .{cap}); + continue; + }; + if (cap < 90 and (m.kind == .lane_gemv or m.kind == .lane_gemv_split)) + std.debug.print("N/A {s} replacement bits: cluster baseline needs sm_90; gate 3 uses the float64 bound\n", .{k.spec.name}); + try kernels.append(a, k); + } var rank = rank_k.Rank{ .rig = .{ .gpu = gpu, .s = s }, .sum = &rank_sum }; defer rank.deinit(); try kernels.append(a, rank.kernel());