diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f23e92f959..27ccccd2eaf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,14 +4,16 @@ Each release's page on GitHub has its notes and measurements. See [the 1.0.0 rel
## Unreleased
+- CUDA EXL3 grouped experts launch and size their storage at the caller's tile and pair count, so decode no longer launches three quarters of its grid empty, and Qwen3.6-35B-A3B EXL3 sizes its grouped storage for 36,864 pair rows instead of 163,904 (about 3 GB less); outputs are bit-equal (#2843, for #2823).
+
+- CUDA: dots.mocr's text decoder serves text prompts on a GB10 from the publisher's BF16 weights, on a new shared engine for dense GQA decoders (`zig/src/cuda_dense/`); `--vision`, prompt caching and fp8 caches are refused by name, and served CUDA checkpoints are unchanged (#2673, part of #1927). Thanks to @laudney.
+
- Tests: main's nightly is green again. The Flash Next learn, prompt and compare tools build again on Linux (#2585 had put one file in two modules), and the dense serving and serving-identity pins take the changes #2579, #2585, #2285, #2205 and #2789 made, each named beside its pin. Serving is unchanged (#3002).
- Cortheon (`--cortheon`): DuckDuckGo results are kept, and its ads still skipped, when only the spelling differs (either quote style, a `class` attribute in any letter case, an unquoted `href`, spaces around its `=`, or a line break before it); a duplicate whose scheme or host differs only in letter case is dropped while paths and queries keep their case, and the PyPI, npm and crates.io allow-list takes the host in any case with the path exact (#2750). Thanks to @ppardus.
- GLM-5.3-Flash on CUDA: with `--vision` and no video library, rank 0 says at startup how much each stream sets aside for `--vision-video-tokens` rows that no request can fill (128 MiB a stream at the default 16,384), and that `--vision-video-tokens 64` on every rank cuts it to 512 KiB (#2813). Thanks to @Bizuayeu.
-
-
- CUDA: `Qwen/Qwen3-Embedding-0.6B`, `-4B` and `-8B` load on the native engine from the published BF16 weights and serve `/v1/embeddings` (last-token pooling, unit-normed, `dimensions` crops). On one GB10 the worst cosine distance to float32 is 1.55e-4, 1.34e-4 and 1.08e-4 (transformers' own BF16: 1.88e-4, 1.79e-4 and 1.77e-4), a request's vectors equal each input sent alone, and a reranker folder or a flag the family does not serve is refused by name (#2285). Thanks to @Arminova.
- Qwen3.8-27B on Metal: the startup line says how many streams draft ("for both streams", "for all 4 streams", "for 8 of 16 streams"), the smaller of the stream count and `TF_Q27_DRAFT_SLOTS`, instead of "for one of them"; log text only (#2806).
diff --git a/README.md b/README.md
index 3c167393198..f74e1ef1e85 100644
--- a/README.md
+++ b/README.md
@@ -177,7 +177,7 @@ Conversions are explicitly marked: their upstream weights need the linked conver
| Model / format | Download | Machine and memory scope | Native recipe / limits |
| --- | --- | --- | --- |
-| dots.mocr, BF16 OCR | Use the local `dots-studio/dots.mocr` checkpoint. | Native HIP; AMD R9700 (`gfx1201`) checked, one GPU. | [Native OCR recipe](docs/recipes/dots-mocr-hip.md); full model-card demo matches HF tokens. No video, MTP head, CUDA, or Metal qualification. |
+| dots.mocr, BF16 OCR | Use the local `dots-studio/dots.mocr` checkpoint. | Native HIP; AMD R9700 (`gfx1201`) checked, one GPU. | [Native OCR recipe](docs/recipes/dots-mocr-hip.md); full model-card demo matches HF tokens. Native CUDA serves text prompts only ([CUDA recipe](docs/recipes/dots-mocr-cuda.md)). No video, MTP head, or Metal qualification. |
| Qwen3.5-2B, MLX 4-bit | `tensorfold pull mlx-community/Qwen3.5-2B-MLX-4bit@93760be4f1f69842a46bc13dbdc0f19e291392a3` | One Mac (M5 Max checked), or one GB10; no RAM minimum published. | [2B](docs/recipes/qwen3.5-2b-native.md); [CUDA release checks](CHANGELOG.md#110). Text only. |
| Qwen3.8-27B, MLX 4-bit | `tensorfold pull TensorFold/Qwen3.8-27B-MLX-4bit`
`tensorfold pull z-lab/Qwen3.8-27B-DFlash2` | One Mac (M5 Max/M3 Ultra), or one GB10; a 32 GB Mac needs `iogpu.wired_limit_mb` raised (the startup refusal names the value). | [27B](docs/recipes/qwen3.8-27b.md). Add `--drafter z-lab/Qwen3.8-27B-DFlash2`; without it decoding is plain. |
| Nemotron 3.5 Lightning 30B-A3B, MLX 4-bit | `tensorfold pull TensorFold/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bit` | One Mac (M1-M5), or one GB10; also checked on RTX 3090 / RTX PRO 6000. | [Lightning](docs/recipes/nemotron-3.5.md). Includes MTP. |
diff --git a/docs/recipes/README.md b/docs/recipes/README.md
index ad33b42053f..50dcf988835 100644
--- a/docs/recipes/README.md
+++ b/docs/recipes/README.md
@@ -8,6 +8,7 @@ For agent-assisted contributions, read the [TensorFold contributor skill](SKILL.
| Family | Recipe |
| --- | --- |
| dots.mocr | [Native HIP OCR, BF16, one GPU](dots-mocr-hip.md) |
+| dots.mocr | [Native CUDA text decoder, BF16, one GPU](dots-mocr-cuda.md) |
| Kimi K3 / K3.1 | [Native Metal bring-up, synthetic only, resident budget refusal](kimi-k3-metal.md) |
| Nemotron 3.5 Lightning | [MLX](nemotron-3.5.md), [Intel Arc, experimental](xpu-arc.md) |
| Nemotron 3.5 Parse | [Native HIP OCR, validation and qualification limits](nemotron-parse.md) |
diff --git a/docs/recipes/dots-mocr-cuda.md b/docs/recipes/dots-mocr-cuda.md
new file mode 100644
index 00000000000..3a218382fec
--- /dev/null
+++ b/docs/recipes/dots-mocr-cuda.md
@@ -0,0 +1,37 @@
+# dots.mocr on native CUDA
+
+The native CUDA engine serves dots.mocr's Qwen2 decoder for text prompts
+through the shared dense engine (`zig/src/cuda_dense/`), from the BF16
+`dots-studio/dots.mocr` checkpoint at revision
+`e539fbb52280393adc081b289ec597430a0f9031`. It is qualified on a GB10
+(sm_121) only. Images are not served on CUDA yet: `--vision` is refused
+by name. The HIP engine, with the image tower, is in
+[dots.mocr on native HIP](dots-mocr-hip.md).
+
+```sh
+zig build native -Dnvcc=/usr/local/cuda/bin/nvcc
+zig-out/native/bin/tensorfold-native serve MODEL_DIRECTORY \
+ --context 8192 --parallel 8 --name dots --host 127.0.0.1 --port 8090
+```
+
+Replies carry no reasoning block: all text is answer content. The engine
+keeps no prompt states, so `--prompt-cache-gib` is refused; so are
+`--drafter`, `--slide`, `--kv-cache fp8` and `--prompt-precision int8`.
+The engine's own copy drafts and `--no-drafts` give the same tokens.
+
+| Responsibility | Files |
+|---|---|
+| Lanes, pages, attention, draws, the host's declarations | `zig/src/cuda_dense/` |
+| Geometry, weights, one layer's arithmetic | `zig/src/families/dots_mocr/cuda_recipe.zig`, `cuda_weights.zig` |
+| q, k and v biases with the rotary in Torch's rounding | `zig/kernels/cuda/dots_rope.cuh` (`tf_dots_qkv_rope`) |
+
+The text reference runs the pinned oracle's `text` mode on any HIP or CUDA
+GPU, and `score` classifies every native id by HF's teacher-forced logits
+(exact, one BF16 step, or a failure, which exits nonzero):
+
+```sh
+python3 tools/zig/dots_mocr_reference.py text MODEL_DIRECTORY REFERENCE \
+ --prompt-file PROMPT.txt --pci PCI_ADDRESS
+python3 tools/zig/dots_mocr_reference.py score MODEL_DIRECTORY REFERENCE \
+ NATIVE_IDS.json DIFFERENCES.json
+```
diff --git a/docs/recipes/dots-mocr-hip.md b/docs/recipes/dots-mocr-hip.md
index 221ff743ea7..68d5701c9e9 100644
--- a/docs/recipes/dots-mocr-hip.md
+++ b/docs/recipes/dots-mocr-hip.md
@@ -3,7 +3,8 @@
This implementation serves the BF16 `dots-studio/dots.mocr`
checkpoint at revision `e539fbb52280393adc081b289ec597430a0f9031`.
It runs the image tower and Qwen2 decoder in native Zig/HIP. Python is
-used only for the validation reference. CUDA and Metal are not qualified.
+used only for the validation reference. CUDA serves the text decoder only
+([dots.mocr on native CUDA](dots-mocr-cuda.md)); Metal is not qualified.
Build for the local GPU architecture. The checked host uses `gfx1201`:
diff --git a/tools/zig/dots_mocr_reference.py b/tools/zig/dots_mocr_reference.py
index 6af268c9734..4c0038cfdb9 100644
--- a/tools/zig/dots_mocr_reference.py
+++ b/tools/zig/dots_mocr_reference.py
@@ -14,6 +14,7 @@
REVISION = "e539fbb52280393adc081b289ec597430a0f9031"
ORACLE_VERSIONS = {"transformers": "4.57.6"}
+ROPE_POSITIONS = [0, 1, 4096, 131071] # the rotary values the native kernel's contract compares, short and long
PROCESSOR_VERSIONS = {"transformers": "4.51.0", "pillow": "11.2.1", "numpy": "2.2.5"}
# The revision's non-weight sources: code, configuration, tokenizer, template, processor and the model card.
SOURCES = {
@@ -67,8 +68,11 @@ def example_prompt(checkpoint):
def selected_pci(torch):
- if torch.version.hip is None or not torch.cuda.is_available() or torch.cuda.device_count() != 1:
- raise RuntimeError("the oracle requires exactly one visible HIP GPU")
+ if not torch.cuda.is_available() or torch.cuda.device_count() != 1:
+ raise RuntimeError("the oracle requires exactly one visible GPU")
+ if torch.version.hip is None:
+ p = torch.cuda.get_device_properties(0)
+ return f"{p.pci_domain_id:04x}:{p.pci_bus_id:02x}:{p.pci_device_id:02x}.0"
lib = ctypes.CDLL("libamdhip64.so")
function = lib.hipDeviceGetPCIBusId
function.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
@@ -79,6 +83,40 @@ def selected_pci(torch):
return text.value.decode().lower()
+class Scores:
+ """Retain the scores used for greedy selection without a growing device tensor history (a LogitsProcessor)."""
+
+ def __init__(self, path, vocab):
+ self.file = path.open("w+b")
+ self.vocab, self.count = vocab, 0
+ self.header()
+
+ def header(self):
+ text = repr({"descr": " std.math.maxInt(i32) or k > std.math.maxInt(i32) or n > std.math.maxInt(i32))
+ pub fn bytes(k: usize, n: usize, pairs: usize, tile: usize, sk: usize, mats: usize) !usize {
+ _ = try tileBlocks(tile);
+ if (pairs > std.math.maxInt(i32) or k > std.math.maxInt(i32) or n > std.math.maxInt(i32))
return error.Exl3Shape;
const x = try std.math.mul(usize, k, 4);
- const z = try std.math.mul(usize, try std.math.mul(usize, n, sk * mats), 4);
- return std.math.mul(usize, p, try std.math.add(usize, x, z));
+ const z = try std.math.mul(usize, try std.math.mul(usize, n, try std.math.mul(usize, sk, mats)), 4);
+ return std.math.mul(usize, pairs, try std.math.add(usize, x, z));
}
- pub fn run(g: *Grouped, s: Stream, epi: experts.Epi, in: experts.Rows, l: experts.Layer, p: grouped.Plan, out: u64, items: usize, skip: i32, _: bool) !void {
+ pub fn run(g: *Grouped, s: Stream, epi: experts.Epi, in: experts.Rows, l: experts.Layer, p: grouped.Plan, out: u64, items: usize, pairs: usize, tile: usize, skip: i32) !void {
+ const mt = try tileBlocks(tile);
if (epi == .up and g.up_contract != .bf16) return error.UnsupportedEpilogue;
const w = l.exl3 orelse return error.BadExperts;
if (w.dims != l.dims or w.width != l.width or w.experts != l.experts or l.relu2 or
@@ -66,17 +67,17 @@ pub const Grouped = struct {
const up = epi == .up;
const k = if (up) w.dims else w.width;
const n = if (up) w.width else w.dims;
- if (k == 0 or n == 0 or k % 128 != 0 or n % 128 != 0 or in.stride < k) return error.Exl3Shape;
+ if (k == 0 or n == 0 or k % 128 != 0 or n % 128 != 0 or in.stride < k or items > std.math.maxInt(i32))
+ return error.Exl3Shape;
const sk = try ex.plan(k, n, up);
const mats: usize = if (up) 2 else 1;
- const size = try bytes(k, n, items, sk, mats);
- if (items == 0) return;
+ const size = try bytes(k, n, pairs, tile, sk, mats);
+ if (items == 0 or pairs == 0) return;
if (g.storage == null or g.storage.?.len < size) {
const replacement = try memory.DeviceBuffer.alloc(g.d, size);
if (g.storage) |*old| old.free();
g.storage = replacement;
}
- const pairs = items * 64;
const x0 = g.storage.?.ptr;
const x1 = x0 + pairs * k * 2;
const z = x1 + pairs * k * 2;
@@ -90,16 +91,25 @@ pub const Grouped = struct {
try launch.launch(g.kernels.plan_rotate, .{ .grid = .{ .x = @intCast(items), .y = @intCast(k / 128), .z = @intCast(mats) }, .block = .{ .x = 32 } }, s, &rot);
var args: launch.Args = .{};
for ([_]u64{ x0, x1, if (up) w.gate else w.down, if (up) w.up else w.down, if (up) w.gate_k2 else w.down_k2, if (up) w.up_k2 else w.down_k2, p.items, p.counts, p.members, z }) |v| args.add(v);
- for ([_]usize{ k, n, pairs, sk, 64, in.slots, items, mats, if (up) w.gu_stride else 0, 1 }) |v|
+ for ([_]usize{ k, n, pairs, sk, tile, in.slots, items, mats, if (up) w.gu_stride else 0, 1 }) |v|
args.add(@as(i32, @intCast(v)));
args.add(skip);
- try launch.launch(g.kernels.grouped[@backingInt(w.codebook)], .{ .grid = .{ .x = @intCast(items), .y = @intCast(n / 128), .z = @intCast(mats * sk * 4) }, .block = .{ .x = 128 } }, s, &args);
+ try launch.launch(g.kernels.grouped[@backingInt(w.codebook)], .{ .grid = .{ .x = @intCast(items), .y = @intCast(n / 128), .z = @intCast(mats * sk * mt) }, .block = .{ .x = 128 } }, s, &args);
var finish: launch.Args = .{};
finish.add(a);
try launch.launch(if (l.after) g.kernels.plan_finish_after else g.kernels.plan_finish, .{ .grid = .{ .x = @intCast(items), .y = @intCast(n / 128) }, .block = .{ .x = 32 } }, s, &finish);
}
};
+/// Member-tile blocks a grouped launch spans: 16-member decode items or 64-member prompt items.
+fn tileBlocks(tile: usize) !usize {
+ return switch (tile) {
+ 16 => 1,
+ 64 => 4,
+ else => error.TileNotBuilt,
+ };
+}
+
/// The activations plan_finish runs (0 bf16, 1 fp32, 2 SwiGLU-OAI); SwiGLU-OAI needs its clamp and alpha.
fn checkAct(w: ex.Layer, after: bool) !void {
if (w.act_mode < 0 or w.act_mode > 2) return error.BadExperts;
@@ -135,16 +145,96 @@ test "Grouped takes the clamp-after order at fp32 activations only: not at bf16,
try std.testing.expectError(error.OptionNotBuilt, checkAct(w, true));
}
-test "Grouped scratch includes every pair and rejects overflowing geometry" {
- try std.testing.expectEqual(@as(usize, 64 * (128 * 4 + 256 * 4 * 2 * 4)), try Grouped.bytes(128, 256, 1, 4, 2));
- try std.testing.expectError(error.Exl3Shape, Grouped.bytes(std.math.maxInt(usize), 256, 1, 4, 2));
- try std.testing.expectError(error.Exl3Shape, Grouped.bytes(128, 256, std.math.maxInt(i32), 4, 2));
+test "Grouped scratch scales linearly with pairs, refuses unsupported tiles, and rejects overflowing geometry" {
+ const one = try Grouped.bytes(2048, 512, 1, 16, 4, 2);
+ try std.testing.expectEqual(@as(usize, 2048 * 4 + 512 * 4 * 2 * 4), one);
+ try std.testing.expectEqual(144 * one, try Grouped.bytes(2048, 512, 144, 16, 4, 2));
+ try std.testing.expectEqual(144 * one, try Grouped.bytes(2048, 512, 144, 64, 4, 2));
+ for ([_]usize{ 0, 8, 15, 17, 32, 48, 128 }) |bad| {
+ try std.testing.expectError(error.TileNotBuilt, Grouped.bytes(2048, 512, 144, bad, 4, 2));
+ }
+ try std.testing.expectError(error.Exl3Shape, Grouped.bytes(std.math.maxInt(usize), 256, 1, 16, 4, 2));
+ try std.testing.expectError(error.Exl3Shape, Grouped.bytes(128, 256, @as(usize, std.math.maxInt(i32)) + 1, 16, 4, 2));
}
test "Grouped's bf16 up refuses a call that asks for the combined fp32 activation pipeline" {
var g: Grouped = .{ .d = undefined, .kernels = undefined, .up_contract = .combined_fp32 };
const layer: experts.Layer = .{ .format = .exl3, .up = 0, .down = 0, .dims = 128, .width = 128, .experts = 1 };
- try std.testing.expectError(error.UnsupportedEpilogue, g.run(undefined, .up, undefined, layer, undefined, 0, 0, -1, false));
+ for ([_]usize{ 0, 8, 32, 128 }) |bad| {
+ try std.testing.expectError(error.TileNotBuilt, g.run(undefined, .up, undefined, layer, undefined, 0, 1, 1, bad, -1));
+ }
+ try std.testing.expectError(error.UnsupportedEpilogue, g.run(undefined, .up, undefined, layer, undefined, 0, 0, 0, 16, -1));
g.up_contract = .bf16;
- try std.testing.expectError(error.BadExperts, g.run(undefined, .up, undefined, layer, undefined, 0, 0, -1, false));
+ try std.testing.expectError(error.BadExperts, g.run(undefined, .up, undefined, layer, undefined, 0, 0, 0, 16, -1));
+}
+
+test "Grouped.run launches the plan tile's grid and compact pair stride, dropping no member at tile 64" {
+ const abi = @import("abi.zig");
+ const Trace = struct {
+ var n: usize = 0;
+ var grids: [3][3]u32 = undefined;
+ var rot: Stage = undefined;
+ var fin: Stage = undefined;
+ var ptrs: [10]u64 = undefined;
+ var ints: [11]i32 = undefined;
+ fn launchFn(_: abi.Function, gx: c_uint, gy: c_uint, gz: c_uint, _: c_uint, _: c_uint, _: c_uint, _: c_uint, _: abi.Stream, args: ?[*]?*anyopaque, _: ?[*]?*anyopaque) callconv(.c) abi.Result {
+ grids[n] = .{ gx, gy, gz };
+ if (n == 0) rot = @as(*const Stage, @ptrCast(@alignCast(args.?[0].?))).*;
+ if (n == 1) {
+ for (&ptrs, 0..) |*p, i| p.* = @as(*const u64, @ptrCast(@alignCast(args.?[i].?))).*;
+ for (&ints, 0..) |*v, i| v.* = @as(*const i32, @ptrCast(@alignCast(args.?[10 + i].?))).*;
+ }
+ if (n == 2) fin = @as(*const Stage, @ptrCast(@alignCast(args.?[0].?))).*;
+ n += 1;
+ return 0;
+ }
+ };
+ var d: Driver = undefined;
+ d.api.cuLaunchKernel = Trace.launchFn;
+ var ks: ex.Experts = undefined;
+ ks.plan_rotate = .{ .d = &d, .handle = @ptrFromInt(1) };
+ ks.plan_finish = .{ .d = &d, .handle = @ptrFromInt(2) };
+ ks.plan_finish_after = .{ .d = &d, .handle = @ptrFromInt(3) };
+ for (&ks.grouped, 0..) |*f, i| f.* = .{ .d = &d, .handle = @ptrFromInt(10 + i) };
+ const base: u64 = 0x4000_0000;
+ var g: Grouped = .{ .d = &d, .kernels = &ks, .storage = .{ .d = &d, .ptr = base, .len = 1 << 28 } };
+ const w: ex.Layer = .{ .gate = 0x10, .up = 0x20, .down = 0x30, .gate_k2 = 0x40, .up_k2 = 0x50, .down_k2 = 0x60, .suh_g = 1, .suh_u = 2, .suh_d = 3, .svh_g = 4, .svh_u = 5, .svh_d = 6, .dims = 2048, .width = 512, .experts = 257, .codebook = .mul1 };
+ const l: experts.Layer = .{ .format = .exl3, .exl3 = w, .up = 0, .down = 0, .dims = 2048, .width = 512, .experts = 257 };
+ const pl: grouped.Plan = .{ .members = 0x100, .items = 0x200, .counts = 0x300, .rank = 0, .hist = 0 };
+ const s: Stream = .{ .d = &d, .handle = null };
+ const grp: experts.Grouped = .{ .exl3 = &g };
+ for ([_]struct { tile: usize, pairs: usize, items: usize, epi: experts.Epi, prompt: bool }{
+ .{ .tile = 16, .pairs = 9, .items = 9, .epi = .up, .prompt = false },
+ .{ .tile = 16, .pairs = 144, .items = 153, .epi = .down_f32, .prompt = false },
+ .{ .tile = 64, .pairs = 512, .items = 136, .epi = .up, .prompt = false },
+ .{ .tile = 64, .pairs = 576, .items = 266, .epi = .down_bf16, .prompt = true },
+ }) |c| {
+ Trace.n = 0;
+ const up = c.epi == .up;
+ const k: usize = if (up) 2048 else 512;
+ const n: usize = if (up) 512 else 2048;
+ const sk: usize = try ex.plan(k, n, up);
+ const mats: usize = if (up) 2 else 1;
+ const in: experts.Rows = .{ .x = 0x9000, .stride = k, .slots = if (up) 9 else 0 };
+ if (c.prompt) try grp.prompt(s, c.epi, in, l, pl, 0xa000, c.items, c.pairs, c.tile, -1) else try grp.decode(s, c.epi, in, l, pl, 0xa000, c.items, c.pairs, c.tile, -1);
+ try std.testing.expectEqual(@as(usize, 3), Trace.n);
+ try std.testing.expectEqual(@as(u32, @intCast(mats * sk * (c.tile / 16))), Trace.grids[1][2]);
+ try std.testing.expectEqual(@as(i32, @intCast(c.pairs)), Trace.rot.p);
+ try std.testing.expectEqual(@as(i32, @intCast(c.pairs)), Trace.fin.p);
+ try std.testing.expectEqual(@as(i32, @intCast(c.pairs)), Trace.ints[2]);
+ try std.testing.expectEqual(@as(i32, @intCast(c.tile)), Trace.ints[4]);
+ try std.testing.expectEqual(base + c.pairs * k * 2, Trace.rot.x1);
+ try std.testing.expectEqual(base + c.pairs * k * 4, Trace.rot.z);
+ try std.testing.expectEqual(Trace.rot.x1, Trace.ptrs[1]);
+ try std.testing.expectEqual(Trace.rot.z, Trace.ptrs[9]);
+ if (c.tile == 64 and up) {
+ var seen: [64]usize = @splat(0);
+ const mt: u32 = @intCast((@as(usize, @intCast(Trace.ints[4])) + 15) / 16);
+ for (0..Trace.grids[1][2]) |zz| {
+ const mtile = zz % mt;
+ for (0..16) |tx| seen[mtile * 16 + tx] += 1;
+ }
+ for (seen) |hits| try std.testing.expectEqual(mats * sk, hits);
+ }
+ }
}
diff --git a/zig/src/cuda/experts.zig b/zig/src/cuda/experts.zig
index 3c5828bea36..3d70527c1d8 100644
--- a/zig/src/cuda/experts.zig
+++ b/zig/src/cuda/experts.zig
@@ -53,11 +53,11 @@ pub const Grouped = struct {
nvfp4_csf0: ?*const @import("experts32.zig").Nvfp4b32Experts = null,
iq2xxs: ?*const Iq2xxsExperts = null,
- /// Decode rows: `items` bounds the plan's items (grouped.maxItems at tile 16); `skip` an expert left out, or -1.
- pub fn decode(g: Grouped, s: Stream, epi: Epi, in: Rows, l: Layer, p: Plan, out: u64, items: usize, skip: i32) !void {
+ /// Decode rows: `items` bounds the plan, `pairs` its picks; `tile` 16 or 64 (EXL3 only); `skip` an expert or -1.
+ pub fn decode(g: Grouped, s: Stream, epi: Epi, in: Rows, l: Layer, p: Plan, out: u64, items: usize, pairs: usize, tile: usize, skip: i32) !void {
if (l.after and l.format != .exl3) return error.OptionNotBuilt; // the clamp-after order is built for EXL3 only
switch (l.format) {
- .exl3 => return (g.exl3 orelse return error.FormatNotLoaded).run(s, epi, in, l, p, out, items, skip, false),
+ .exl3 => return (g.exl3 orelse return error.FormatNotLoaded).run(s, epi, in, l, p, out, items, pairs, tile, skip),
.affine4 => {
const a = g.affine4 orelse return error.FormatNotLoaded;
if (epi == .down_bf16) return error.EpilogueNotBuilt;
@@ -80,11 +80,11 @@ pub const Grouped = struct {
}
}
- /// Prompt rows: `items` bounds the plan's items at the format's prompt tile (prompt_tile).
- pub fn prompt(g: Grouped, s: Stream, epi: Epi, in: Rows, l: Layer, p: Plan, out: u64, items: usize, skip: i32) !void {
+ /// Prompt rows: `items` bounds the plan, `pairs` its picks; `tile` 16 or 64 (EXL3 only); `skip` an expert or -1.
+ pub fn prompt(g: Grouped, s: Stream, epi: Epi, in: Rows, l: Layer, p: Plan, out: u64, items: usize, pairs: usize, tile: usize, skip: i32) !void {
if (l.after and l.format != .exl3) return error.OptionNotBuilt; // the clamp-after order is built for EXL3 only
switch (l.format) {
- .exl3 => return (g.exl3 orelse return error.FormatNotLoaded).run(s, epi, in, l, p, out, items, skip, true),
+ .exl3 => return (g.exl3 orelse return error.FormatNotLoaded).run(s, epi, in, l, p, out, items, pairs, tile, skip),
.affine4 => {
const a = g.affine4 orelse return error.FormatNotLoaded;
if (epi == .down_f32) return error.EpilogueNotBuilt;
@@ -556,8 +556,8 @@ test "nvfp4 rows of either kind are refused when the format is not loaded" {
const l: Layer = .{ .format = .nvfp4, .up = 0, .down = 0, .width = 64, .dims = 64, .experts = 2, .relu2 = true };
const s: Stream = undefined; // never reached: the refusal comes first
const p: Plan = .{ .members = 0, .items = 0, .counts = 0, .rank = 0, .hist = 0 };
- try std.testing.expectError(error.FormatNotLoaded, g.prompt(s, .up, .{ .x = 0, .stride = 64 }, l, p, 0, 1, -1));
- try std.testing.expectError(error.FormatNotLoaded, g.decode(s, .down_bf16, .{ .x = 0, .stride = 64 }, l, p, 0, 1, -1));
+ try std.testing.expectError(error.FormatNotLoaded, g.prompt(s, .up, .{ .x = 0, .stride = 64 }, l, p, 0, 1, 1, 64, -1));
+ try std.testing.expectError(error.FormatNotLoaded, g.decode(s, .down_bf16, .{ .x = 0, .stride = 64 }, l, p, 0, 1, 1, 16, -1));
try std.testing.expectEqual(@as(usize, 2 * 2 * 3 * 144 * 4), Nvfp4Experts.bytes(2, 64, 96, 1));
try std.testing.expectEqual(@as(u32, 2 * 17408), Nvfp4Experts.promptSmem(1, 8)); // 64 rows x 64 inputs, 8 x 2 blocks
try std.testing.expectEqual(@as(u32, 2 * 17408), Nvfp4Experts.promptSmem(2, 4));
diff --git a/zig/src/cuda/experts_host_test.zig b/zig/src/cuda/experts_host_test.zig
index 1b645b64f84..635eea49d2f 100644
--- a/zig/src/cuda/experts_host_test.zig
+++ b/zig/src/cuda/experts_host_test.zig
@@ -12,15 +12,15 @@ test "a format that is not loaded, or an option it was not built with, is refuse
const l: Layer = .{ .format = .fp8g, .up = 0, .down = 0, .width = 64, .dims = 64, .experts = 2 };
const s: Stream = undefined; // never reached: the refusal comes first
const p: Plan = .{ .members = 0, .items = 0, .counts = 0, .rank = 0, .hist = 0 };
- try std.testing.expectError(error.FormatNotLoaded, g.decode(s, .up, .{ .x = 0, .stride = 64 }, l, p, 0, 1, -1));
+ try std.testing.expectError(error.FormatNotLoaded, g.decode(s, .up, .{ .x = 0, .stride = 64 }, l, p, 0, 1, 1, 16, -1));
const a4: experts.Affine4Experts = undefined;
const h: Grouped = .{ .affine4 = &a4 };
var la = l;
la.format = .affine4;
- try std.testing.expectError(error.EpilogueNotBuilt, h.decode(s, .down_bf16, .{ .x = 0, .stride = 64 }, la, p, 0, 1, -1));
- try std.testing.expectError(error.EpilogueNotBuilt, h.prompt(s, .down_f32, .{ .x = 0, .stride = 64 }, la, p, 0, 1, -1));
- try std.testing.expectError(error.OptionNotBuilt, h.decode(s, .up, .{ .x = 0, .stride = 64 }, la, p, 0, 1, 3));
- try std.testing.expectError(error.OptionNotBuilt, h.prompt(s, .up, .{ .x = 0, .stride = 64 }, .{ .format = .affine4, .up = 0, .down = 0, .width = 64, .dims = 64, .experts = 2, .after = true }, p, 0, 1, -1));
+ try std.testing.expectError(error.EpilogueNotBuilt, h.decode(s, .down_bf16, .{ .x = 0, .stride = 64 }, la, p, 0, 1, 1, 16, -1));
+ try std.testing.expectError(error.EpilogueNotBuilt, h.prompt(s, .down_f32, .{ .x = 0, .stride = 64 }, la, p, 0, 1, 1, 64, -1));
+ try std.testing.expectError(error.OptionNotBuilt, h.decode(s, .up, .{ .x = 0, .stride = 64 }, la, p, 0, 1, 1, 16, 3));
+ try std.testing.expectError(error.OptionNotBuilt, h.prompt(s, .up, .{ .x = 0, .stride = 64 }, .{ .format = .affine4, .up = 0, .down = 0, .width = 64, .dims = 64, .experts = 2, .after = true }, p, 0, 1, 1, 64, -1));
}
test "the clamp-after order: exl3 takes it on both paths, every other format refuses it on both" {
@@ -30,11 +30,11 @@ test "the clamp-after order: exl3 takes it on both paths, every other format ref
const in: experts.Rows = .{ .x = 0, .stride = 64 };
var l: Layer = .{ .format = .exl3, .up = 0, .down = 0, .width = 64, .dims = 64, .experts = 2, .limit = 10, .after = true };
// exl3 accepts the order, then finds its kernels not loaded
- try std.testing.expectError(error.FormatNotLoaded, g.decode(s, .up, in, l, p, 0, 1, -1));
- try std.testing.expectError(error.FormatNotLoaded, g.prompt(s, .up, in, l, p, 0, 1, -1));
+ try std.testing.expectError(error.FormatNotLoaded, g.decode(s, .up, in, l, p, 0, 1, 1, 16, -1));
+ try std.testing.expectError(error.FormatNotLoaded, g.prompt(s, .up, in, l, p, 0, 1, 1, 64, -1));
for (std.enums.values(experts.Format)) |f| if (f != .exl3) {
l.format = f; // nvfp4 among them; not loaded either, so the refusal comes before the format's
- try std.testing.expectError(error.OptionNotBuilt, g.decode(s, .up, in, l, p, 0, 1, -1));
- try std.testing.expectError(error.OptionNotBuilt, g.prompt(s, .up, in, l, p, 0, 1, -1));
+ try std.testing.expectError(error.OptionNotBuilt, g.decode(s, .up, in, l, p, 0, 1, 1, 16, -1));
+ try std.testing.expectError(error.OptionNotBuilt, g.prompt(s, .up, in, l, p, 0, 1, 1, 64, -1));
};
}
diff --git a/zig/src/cuda_dense/backend.zig b/zig/src/cuda_dense/backend.zig
new file mode 100644
index 00000000000..7a1e5ba3be0
--- /dev/null
+++ b/zig/src/cuda_dense/backend.zig
@@ -0,0 +1,350 @@
+//! A dense decoder behind lanes.Backend: a slot and pool pages a stream, a chunk a prompt step, one forward a round.
+const std = @import("std");
+const cuda = @import("cuda");
+const lanes = @import("lanes");
+const kv = @import("core").kv_pool;
+const pass = @import("pass.zig");
+const mdl = @import("model.zig");
+const kern = @import("kernels.zig");
+const pages = @import("pages.zig");
+const draws_ = @import("draws.zig");
+
+const Draws = draws_.Draws;
+const ring = draws_.ring;
+const be = lanes.backend;
+const Stream = lanes.Stream;
+
+/// Prompt rows a step takes: the forward's chunk grid, from position 0.
+pub const prompt_rows = mdl.prompt_rows;
+/// Window rows a round takes over every stream.
+pub const round_rows = mdl.round_rows;
+/// The widest window a stream verifies: one-row steps and every wider window give a row the same bits.
+pub const max_window = pages.window;
+
+const Lane = struct {
+ slot: usize,
+ len: u64 = 0, // the cache length: positions [0, len) hold this stream's keys
+ base: u64 = 0, // the cache length the last verify started from
+ rows: std.ArrayList(u32) = .empty, // the last verify's row tokens
+ last: u64, // the prompt's last row's bf16 logits [V] (the first draw reads them)
+ rule: u64, // sample.cu's Rule, when the stream samples
+ sampled: bool = false,
+ fill: ?usize = null, // a prompt pass going a chunk at a time: where its next chunk starts
+ lease: kv.Lease = .{}, // the pages it holds and is promised
+};
+
+pub fn Dense(comptime R: type) type {
+ return struct {
+ const D = @This();
+ gpa: std.mem.Allocator,
+ d: *const cuda.Driver,
+ e: pass.Engine(R),
+ m: *mdl.Model,
+ lanes: std.AutoHashMapUnmanaged(*const Stream, Lane) = .empty,
+ free: []bool, // slot i is free
+ draws: Draws,
+ slot_bufs: []cuda.DeviceBuffer, // a slot's last-row logits, then its rule: two a slot
+ pool: ?kv.Pool = null, // the pages every slot leases (attach)
+
+ /// The device bytes `init` allocates: the draws and a round's slots' last rows and rules.
+ pub fn openBytes(vocab: usize) usize {
+ return Draws.openBytes() + round_rows * (vocab * 2 + @sizeOf(cuda.sample.Rule));
+ }
+
+ pub fn init(gpa: std.mem.Allocator, d: *const cuda.Driver, m: *mdl.Model, k: *const kern.Kernels, own: *const R.Kernels, w: *const R.Weights, s: cuda.Stream) !D {
+ const free = try gpa.alloc(bool, round_rows);
+ errdefer gpa.free(free);
+ @memset(free, true);
+ var draws = try Draws.init(gpa, d, k, s, m.g.vocab);
+ errdefer draws.deinit(gpa);
+ const bufs = try gpa.alloc(cuda.DeviceBuffer, 2 * round_rows);
+ errdefer gpa.free(bufs);
+ var made: usize = 0;
+ errdefer for (bufs[0..made]) |*b| b.free();
+ for (bufs, 0..) |*b, i| {
+ b.* = try cuda.DeviceBuffer.alloc(d, if (i % 2 == 0) m.g.vocab * 2 else @sizeOf(cuda.sample.Rule));
+ made += 1;
+ }
+ return .{ .gpa = gpa, .d = d, .e = .{ .m = m, .k = k, .own = own, .w = w, .s = s }, .m = m, .free = free, .draws = draws, .slot_bufs = bufs };
+ }
+
+ pub fn deinit(x: *D) void {
+ var it = x.lanes.valueIterator();
+ while (it.next()) |l| {
+ if (x.pool) |*p| p.finish(&l.lease);
+ l.rows.deinit(x.gpa);
+ }
+ if (x.pool) |*p| p.deinit();
+ x.lanes.deinit(x.gpa);
+ for (x.slot_bufs) |*b| b.free();
+ x.gpa.free(x.slot_bufs);
+ x.gpa.free(x.free);
+ x.draws.deinit(x.gpa);
+ }
+
+ /// The model's arena of `n` pages for `extra + 1` slots, and the pool that leases them.
+ pub fn attach(x: *D, extra: usize, n: usize) !void {
+ if (x.pool != null) return error.BadAttach;
+ try x.m.attach(x.e.s, extra, n);
+ x.pool = try kv.Pool.init(x.gpa, n, x.m.pageBytes());
+ }
+
+ /// The pool's free pages in a seeded random order, leased in that order (a check's hook).
+ pub fn shufflePages(x: *D, seed: u64) void {
+ var prng = std.Random.DefaultPrng.init(seed);
+ if (x.pool) |*p| prng.random().shuffle(u32, p.free[0..p.nfree]);
+ }
+
+ pub fn backend(x: *D) be.Backend {
+ return .{ .ptr = x, .vtable = &vtable };
+ }
+
+ const vtable: be.Backend.VTable = .{ .prefill = prefill, .prefill_step = prefillStep, .cancel_prefill = cancelPrefill, .first = first, .queue = queue, .read = read, .verify = verify, .keep = keep, .draft = draft, .release = release, .reports_fill = true };
+
+ /// The round loop's facts: no draft head; host copies in windows up to 16 rows, streams sharing a forward.
+ pub fn facts(_: *const D) lanes.Model {
+ return .{ .exact_width = max_window, .gpu_tokens = false, .mtp = false, .speculate = false, .hidden_rows = true, .batch_rows = round_rows, .max_streams = round_rows };
+ }
+
+ fn self(ptr: *anyopaque) *D {
+ return @ptrCast(@alignCast(ptr));
+ }
+
+ fn forward(x: *D, chains: []const pass.Chain, prompt: bool, head0: usize, heads: usize) !void {
+ try pass.forward(R, x.e, chains, prompt, head0, heads);
+ }
+
+ fn freeSlot(x: *const D) ?usize {
+ return std.mem.indexOfScalar(bool, x.free[0..x.m.slots], true);
+ }
+
+ /// Whether a request could start now: a free slot and its pages; one that never fits passes (refused later).
+ pub fn admits(x: *const D, prompt: usize, max_new: usize) bool {
+ if (prompt + max_new > x.m.context) return true;
+ const p = x.pool orelse return false;
+ return x.freeSlot() != null and p.fits(pages.pagesFor(prompt, max_new));
+ }
+
+ const Map = struct {
+ x: *D,
+ slot: usize,
+ fn write(ctx: *anyopaque, ids: []const u32, from: usize) anyerror!void {
+ const m: *const Map = @ptrCast(@alignCast(ctx));
+ try m.x.m.mapPages(m.x.e.s, m.slot, ids[from..], from);
+ }
+ };
+
+ /// The lane's pages cover positions [0, end): pages taken from its promise as its context grows.
+ fn reach(x: *D, l: *Lane, end: usize) !void {
+ const p = if (x.pool) |*p| p else return error.NoPages;
+ const n = pages.pagesOf(end);
+ if (n <= l.lease.ids.items.len) return;
+ var map: Map = .{ .x = x, .slot = l.slot };
+ try p.cover(&l.lease, n, .{ .ctx = &map, .write = Map.write });
+ }
+
+ /// A free slot for the request, its pages promised now (taken as its rows need them).
+ fn takeSlot(x: *D, lease: *kv.Lease, prompt: usize, max_new: usize) !usize {
+ const p = if (x.pool) |*p| p else return error.NoPages;
+ const slot = x.freeSlot() orelse return error.NoFreeSlot;
+ try p.promise(lease, pages.pagesFor(prompt, max_new));
+ return slot;
+ }
+
+ /// A prompt pass begins: a slot, its pages, the stream's rule; no prompt cache, so its first chunk starts at 0.
+ fn opening(x: *D, s: *Stream) !*Lane {
+ const p = s.prompt();
+ if (p.len == 0) return error.EmptyPrompt;
+ if (s.images.len > 0) return error.DenseTextOnly;
+ if (p.len + @as(usize, s.max_new) > x.m.context) return error.PastContext;
+ const got = try x.lanes.getOrPut(x.gpa, s);
+ if (!got.found_existing) {
+ var lease: kv.Lease = .{};
+ const slot = x.takeSlot(&lease, p.len, s.max_new) catch |e| {
+ x.lanes.removeByPtr(got.key_ptr);
+ return e;
+ };
+ x.free[slot] = false;
+ got.value_ptr.* = .{ .slot = slot, .last = x.slot_bufs[2 * slot].ptr, .rule = x.slot_bufs[2 * slot + 1].ptr, .lease = lease };
+ }
+ const l = got.value_ptr;
+ try x.reach(l, p.len);
+ l.sampled = false;
+ if (s.sampling) |sp| if (sp.temperature > 0) {
+ const r = cuda.sample.Rule.from(lanes.gpu_rule.rule(sp));
+ try mdl.put(x.d, x.e.s, l.rule, std.mem.asBytes(&r));
+ l.sampled = true;
+ };
+ s.cached = 0;
+ if (s.reuse.saved != null) s.reuse_failed = true; // no prompt cache: every pass starts at 0
+ l.fill = 0;
+ s.fill_at.store(0, .monotonic);
+ return l;
+ }
+
+ /// One chunk from `l.fill` to the next grid line or mark; true once the prompt's last row's logits are written.
+ fn chunk(x: *D, s: *Stream, l: *Lane) !bool {
+ const p = s.prompt();
+ const at = l.fill.?;
+ if (s.isCancelled()) {
+ s.keepInterruptedPrefill(@intCast(at));
+ return error.Cancelled;
+ }
+ var end = @min(p.len, (at / prompt_rows + 1) * prompt_rows);
+ for (s.reuse.marks) |mk| if (mk > at and mk < end) {
+ end = mk;
+ };
+ const done = end == p.len;
+ try x.forward(&.{.{ .slot = l.slot, .p0 = at, .tokens = p[at..end] }}, true, end - at - 1, @intFromBool(done));
+ if (done) try x.d.check(x.d.api.cuMemcpyDtoDAsync_v2(l.last, x.m.s.logits, x.m.g.vocab * 2, x.e.s.handle), "cuMemcpyDtoDAsync");
+ s.fill_at.store(@intCast(end), .monotonic);
+ l.fill = if (done) null else end;
+ if (done) l.len = p.len;
+ if (s.reuse.hook) |hk| if (std.mem.indexOfScalar(u32, s.reuse.marks, @intCast(end)) != null) hk.at(hk.ptr, s, @intCast(end));
+ return done;
+ }
+
+ fn prefill(ptr: *anyopaque, s: *Stream) anyerror!void {
+ const x = self(ptr);
+ const l = try x.opening(s);
+ errdefer x.dropLane(s);
+ while (!try x.chunk(s, l)) {}
+ }
+
+ fn prefillStep(ptr: *anyopaque, s: *Stream, _: u32) anyerror!bool {
+ const x = self(ptr);
+ const going = if (x.lanes.getPtr(s)) |l| l.fill != null else false;
+ const l = if (going) x.lanes.getPtr(s).? else try x.opening(s);
+ errdefer x.dropLane(s);
+ return x.chunk(s, l);
+ }
+
+ fn cancelPrefill(ptr: *anyopaque, s: *Stream) !void {
+ const l = self(ptr).lanes.getPtr(s) orelse return;
+ if (l.fill) |at| s.keepInterruptedPrefill(@intCast(at));
+ }
+
+ fn first(ptr: *anyopaque, s: *Stream, position: u64) anyerror!u64 {
+ const x = self(ptr);
+ const l = x.lanes.getPtr(s) orelse return error.UnknownStream;
+ if (position != l.len or l.fill != null) return error.PositionMismatch;
+ const h = x.draws.handle();
+ try x.draws.rows(l.sampled, l.rule, l.last, 0, 1, position, h.at, 0);
+ return h.h;
+ }
+
+ fn value(x: *D, feed: be.Feed) !u32 {
+ return switch (feed) {
+ .value => |v| v,
+ .handle => |h| x.draws.read(h),
+ };
+ }
+
+ fn queue(ptr: *anyopaque, s: *Stream, feed: be.Feed, position: u64) anyerror!u64 {
+ const x = self(ptr);
+ const l = x.lanes.getPtr(s) orelse return error.UnknownStream;
+ const t = try x.value(feed);
+ if (position != l.len + 1) return error.PositionMismatch;
+ try x.reach(l, l.len + 1);
+ try x.forward(&.{.{ .slot = l.slot, .p0 = l.len, .tokens = &.{t} }}, false, 0, 1);
+ l.len += 1;
+ const h = x.draws.handle();
+ try x.draws.rows(l.sampled, l.rule, x.m.s.logits, 0, 1, position, h.at, 0);
+ return h.h;
+ }
+
+ fn read(ptr: *anyopaque, h: u64) anyerror!u32 {
+ return self(ptr).draws.read(h);
+ }
+
+ fn verify(ptr: *anyopaque, windows: []const be.Window, out: []be.Verified) anyerror!void {
+ const x = self(ptr);
+ if (windows.len > round_rows) return error.RoundTooWide;
+ const chains = try x.gpa.alloc(pass.Chain, windows.len);
+ defer x.gpa.free(chains);
+ var total: usize = 0;
+ for (windows, chains) |w, *c| {
+ if (w.held != 0) return error.NoHeldDrafts; // no draft head: every draft comes from the host
+ if (w.parents != null) return error.ChainsOnly;
+ if (w.masks.len != 0) return error.StructuresUnsupported;
+ const l = x.lanes.getPtr(w.stream) orelse return error.UnknownStream;
+ if (w.rows() > max_window) return error.WindowTooWide;
+ l.base = l.len;
+ l.rows.clearRetainingCapacity();
+ try l.rows.append(x.gpa, w.pending);
+ try l.rows.appendSlice(x.gpa, w.tokens);
+ for (w.positions, 0..) |pos, r| if (pos != l.base + r + 1) return error.PositionMismatch;
+ try x.reach(l, l.base + l.rows.items.len);
+ c.* = .{ .slot = l.slot, .p0 = l.base, .tokens = l.rows.items };
+ total += l.rows.items.len;
+ }
+ if (total > round_rows) return error.RoundTooWide;
+ try x.forward(chains, false, 0, total);
+ var at: usize = 0;
+ var sampled = false;
+ for (windows) |w| {
+ const l = x.lanes.getPtr(w.stream).?;
+ try x.draws.pick(l.sampled, x.m.s.logits, at, l.rows.items.len, ring + at);
+ sampled = sampled or l.sampled;
+ at += l.rows.items.len;
+ }
+ if (sampled) try x.draws.finite(ring, total); // one wait a round, before any draw reads a row
+ at = 0;
+ for (windows, 0..) |w, i| {
+ const l = x.lanes.getPtr(w.stream).?;
+ if (l.sampled) try x.draws.draw(l.rule, x.m.s.logits, at, l.rows.items.len, w.positions[0], ring + at, i);
+ at += l.rows.items.len;
+ }
+ var got: [round_rows]u32 = undefined;
+ try x.draws.tokens(ring, got[0..total]);
+ at = 0;
+ for (windows, out) |w, o| {
+ const l = x.lanes.getPtr(w.stream).?;
+ const n = l.rows.items.len;
+ @memcpy(o.sampled[0..n], got[at..][0..n]);
+ @memcpy(o.drafts[0 .. n - 1], l.rows.items[1..]);
+ l.len = l.base + n; // a chain keeps every row unless rolled back
+ at += n;
+ }
+ }
+
+ fn keep(ptr: *anyopaque, windows: []const be.Window, paths: []const []const u32) anyerror!void {
+ const x = self(ptr);
+ for (windows, paths) |w, path| {
+ const l = x.lanes.getPtr(w.stream) orelse return error.UnknownStream;
+ if (path.len == 0 or path.len > l.rows.items.len) return error.EmptyPath;
+ for (path, 0..) |r, j| if (r != j) return error.ChainsOnly; // a chain keeps a prefix of its rows
+ l.len = l.base + path.len; // caches are positional: later rows overwrite what the rejected ones wrote
+ }
+ }
+
+ /// No draft head: absorbing is a no-op, a request for drafts is refused by name (copies come from the host).
+ fn draft(_: *anyopaque, requests: []const be.DraftRequest) anyerror!void {
+ for (requests) |r| if (r.depth != 0 or r.lanes != null) return error.NoDraftHead;
+ }
+
+ fn dropLane(x: *D, s: *const Stream) void {
+ if (x.lanes.fetchRemove(s)) |entry| {
+ var l = entry.value;
+ if (x.pool) |*p| p.finish(&l.lease); // one CUDA stream: a later lease's writes queue behind these reads
+ x.m.unmapPages(l.slot);
+ x.free[l.slot] = true;
+ l.rows.deinit(x.gpa);
+ if (x.lanes.count() == 0) x.report();
+ }
+ }
+
+ /// The idle server's line: nothing promised, every page free.
+ fn report(x: *const D) void {
+ if (@import("builtin").is_test) return;
+ const p = x.pool orelse return;
+ const c = p.census();
+ const ok = p.idle() and c.free == c.total;
+ std.log.info("kv pool idle{s}: promised {d}, free {d} == total {d}", .{ if (ok) "" else " (UNEVEN)", c.promised, c.free, c.total });
+ }
+
+ fn release(ptr: *anyopaque, s: *Stream) void {
+ self(ptr).dropLane(s);
+ }
+ };
+}
diff --git a/zig/src/cuda_dense/device_fail_test.zig b/zig/src/cuda_dense/device_fail_test.zig
new file mode 100644
index 00000000000..7fbc24e48b8
--- /dev/null
+++ b/zig/src/cuda_dense/device_fail_test.zig
@@ -0,0 +1,202 @@
+//! Failure paths on a recording fake device: no buffer freed before completion, no draw over non-finite logits.
+const std = @import("std");
+const cuda = @import("cuda");
+const mdl = @import("model.zig");
+const kern = @import("kernels.zig");
+const draws = @import("draws.zig");
+const Geometry = @import("recipe.zig").Geometry;
+const t = std.testing;
+
+const Fake = struct {
+ const queue: cuda.abi.Stream = @ptrFromInt(0x1800);
+ const base: u64 = 0x100000;
+ var mem: [4096]u8 = undefined; // device bytes from `base` on, for downloads
+ var next: u64 = 0x200000;
+ var live: usize = 0;
+ var pending = false;
+ var freed_pending: usize = 0;
+ var proofs: usize = 0;
+ var fail_wait = false;
+ var argmax: usize = 0;
+ var drawn: usize = 0;
+ fn reset() void {
+ live = 0;
+ pending = false;
+ freed_pending = 0;
+ proofs = 0;
+ fail_wait = false;
+ argmax = 0;
+ drawn = 0;
+ }
+ fn alloc(out: *u64, len: usize) callconv(.c) c_int {
+ out.* = next;
+ next += len + 256;
+ live += 1;
+ return 0;
+ }
+ fn free(_: u64) callconv(.c) c_int {
+ freed_pending += @intFromBool(pending);
+ live -= 1;
+ return 0;
+ }
+ fn fill(_: u64, _: u8, _: usize, _: cuda.abi.Stream) callconv(.c) c_int {
+ pending = true;
+ return 0;
+ }
+ fn host(out: *?*anyopaque, len: usize, _: c_uint) callconv(.c) c_int {
+ out.* = std.c.malloc(len) orelse return cuda.abi.error_not_ready;
+ return 0;
+ }
+ fn hostFree(p: ?*anyopaque) callconv(.c) c_int {
+ std.c.free(p);
+ return 0;
+ }
+ fn copy(_: u64, _: ?*const anyopaque, _: usize, _: cuda.abi.Stream) callconv(.c) c_int {
+ pending = true;
+ return 0;
+ }
+ fn wait(_: cuda.abi.Stream) callconv(.c) c_int {
+ if (fail_wait) return cuda.abi.error_not_ready;
+ pending = false;
+ return 0;
+ }
+ fn context() callconv(.c) c_int {
+ proofs += 1;
+ pending = false;
+ return 0;
+ }
+ fn event(out: *cuda.abi.Event, _: c_uint) callconv(.c) c_int {
+ out.* = @ptrFromInt(1);
+ return 0;
+ }
+ fn record(_: cuda.abi.Event, _: cuda.abi.Stream) callconv(.c) c_int {
+ return 0;
+ }
+ fn query(_: cuda.abi.Event) callconv(.c) c_int {
+ return if (pending) cuda.abi.error_not_ready else 0;
+ }
+ fn destroy(_: cuda.abi.Event) callconv(.c) c_int {
+ return 0;
+ }
+ fn launch(f: cuda.abi.Function, _: c_uint, _: c_uint, _: c_uint, _: c_uint, _: c_uint, _: c_uint, _: c_uint, _: cuda.abi.Stream, _: ?[*]?*anyopaque, _: ?[*]?*anyopaque) callconv(.c) c_int {
+ argmax += @intFromBool(f == @as(cuda.abi.Function, @ptrFromInt(1)));
+ drawn += @intFromBool(f == @as(cuda.abi.Function, @ptrFromInt(2)));
+ return 0;
+ }
+ fn down(dst: ?*anyopaque, src: u64, len: usize) callconv(.c) c_int {
+ @memcpy(@as([*]u8, @ptrCast(dst.?))[0..len], mem[src - base ..][0..len]);
+ return 0;
+ }
+ fn device(out: *cuda.abi.Device) callconv(.c) c_int {
+ out.* = 0;
+ return 0;
+ }
+ fn attribute(out: *c_int, _: cuda.abi.DeviceAttribute, _: cuda.abi.Device) callconv(.c) c_int {
+ out.* = 0;
+ return 0;
+ }
+ fn driver() cuda.Driver {
+ reset();
+ var d: cuda.Driver = .{ .lib = undefined, .api = undefined };
+ d.api.cuMemAlloc_v2 = alloc;
+ d.api.cuMemFree_v2 = free;
+ d.api.cuMemsetD8Async = fill;
+ d.api.cuMemHostAlloc = host;
+ d.api.cuMemFreeHost = hostFree;
+ d.api.cuMemcpyHtoDAsync_v2 = copy;
+ d.api.cuStreamSynchronize = wait;
+ d.api.cuCtxSynchronize = context;
+ d.api.cuEventCreate = event;
+ d.api.cuEventRecord = record;
+ d.api.cuEventQuery = query;
+ d.api.cuEventDestroy_v2 = destroy;
+ d.api.cuLaunchKernel = launch;
+ d.api.cuMemcpyDtoH_v2 = down;
+ d.api.cuCtxGetDevice = device;
+ d.api.cuDeviceGetAttribute = attribute;
+ return d;
+ }
+};
+
+const tiny: Geometry = .{ .hidden = 256, .layers = 2, .heads = 4, .kv_heads = 2, .head_dim = 64, .vocab = 512, .eps = 1e-6, .context = 4096, .acc = 512, .wide = 1024, .mid = 512, .theta = 1e4 };
+
+fn lane() cuda.qmmf.Lane {
+ var l: cuda.qmmf.Lane = undefined;
+ l.clusters = false; // partBytes reads the tile plan alone
+ return l;
+}
+
+test "a failed scratch upload wait proves completion by the context before init frees its buffers" {
+ var d = Fake.driver();
+ const s: cuda.Stream = .{ .d = &d, .handle = Fake.queue };
+ const l = lane();
+ Fake.fail_wait = true;
+ try t.expectError(error.NotReady, mdl.Model.init(t.allocator, &d, s, tiny, 4096, &l));
+ try t.expectEqual(@as(usize, 0), Fake.freed_pending);
+ try t.expectEqual(@as(usize, 0), Fake.live);
+ try t.expect(Fake.proofs >= 1);
+ Fake.fail_wait = false;
+ try s.synchronize();
+ cuda.copy_source.dropSpares(&d, cuda.copy_source.current());
+}
+
+test "a failed attach wait proves completion before its arena and tables go, and leaves the model attachable" {
+ var d = Fake.driver();
+ const s: cuda.Stream = .{ .d = &d, .handle = Fake.queue };
+ const l = lane();
+ var m = try mdl.Model.init(t.allocator, &d, s, tiny, 4096, &l);
+ const held = Fake.live;
+ Fake.fail_wait = true;
+ try t.expectError(error.NotReady, m.attach(s, 1, 2));
+ try t.expectEqual(@as(usize, 0), Fake.freed_pending);
+ try t.expectEqual(held, Fake.live);
+ try t.expect(m.pages == 0 and m.slots == 0 and m.ids.len == 0 and m.arena == 0);
+ Fake.fail_wait = false;
+ try m.attach(s, 1, 2);
+ try t.expect(m.pages == 2 and m.slots == 2);
+ m.deinit();
+ try t.expectEqual(@as(usize, 0), Fake.live);
+ cuda.copy_source.dropSpares(&d, cuda.copy_source.current());
+}
+
+fn buffer(d: *const cuda.Driver, offset: u64, len: usize) cuda.DeviceBuffer {
+ return .{ .d = d, .ptr = Fake.base + offset, .len = len, .borrowed = true };
+}
+
+fn function(d: *const cuda.Driver, id: usize) cuda.Function {
+ return .{ .d = d, .handle = @ptrFromInt(id) };
+}
+
+test "a sampled row over NaN or an infinity is refused before any draw reads it, as a greedy one is" {
+ var d = Fake.driver();
+ const s: cuda.Stream = .{ .d = &d, .handle = Fake.queue };
+ var k: kern.Kernels = undefined;
+ k.round = .{ .argmax_fn = function(&d, 1), .match_fn = function(&d, 9), .keep_fn = function(&d, 9), .gather_fn = function(&d, 9), .head_fn = function(&d, 9), .ids_fn = function(&d, 9) };
+ k.draws = .{ .draw_fn = function(&d, 2), .draw_ids_fn = function(&d, 9), .logprob_rows_fn = function(&d, 9), .topk_slices_fn = function(&d, 9), .topk_merge_fn = function(&d, 9) };
+ var greedy: [draws.ring + mdl.round_rows]bool = @splat(true);
+ var x: draws.Draws = .{ .d = &d, .k = &k, .s = s, .vocab = 512, .tok = buffer(&d, 0, 64), .picks = buffer(&d, 64, 128), .greedy = &greedy, .meta = buffer(&d, 512, 64) };
+ const Pick = cuda.tree_round.Pick;
+ var picks = [2]Pick{ .{ .token = 3, .nonfinite = 0 }, .{ .token = 4, .nonfinite = 0 } };
+ @memcpy(Fake.mem[64..][0..16], std.mem.sliceAsBytes(&picks)); // what the argmax writes for finite rows
+ @memcpy(Fake.mem[0..8], std.mem.sliceAsBytes(&[2]u32{ 7, 8 }));
+ try x.rows(true, 0x9000, 0xa000, 0, 2, 5, 0, 0);
+ try t.expectEqual(@as(usize, 1), Fake.argmax); // the sampled rows' non-finite flags
+ try t.expectEqual(@as(usize, 1), Fake.drawn);
+ var out: [2]u32 = undefined;
+ try x.tokens(0, &out);
+ try t.expectEqualSlices(u32, &.{ 7, 8 }, &out); // drawn, not picked
+ for ([_]u32{ 1, 2, 4 }) |mask| { // NaN, +inf, -inf (tree_round.cu's mask)
+ picks[1] = .{ .token = 0xFFFFFFFF, .nonfinite = mask };
+ @memcpy(Fake.mem[64..][0..16], std.mem.sliceAsBytes(&picks));
+ try t.expectError(error.NonFiniteLogits, x.rows(true, 0x9000, 0xa000, 0, 2, 5, 0, 0));
+ try t.expectEqual(@as(usize, 1), Fake.drawn); // tf_draw never saw the row
+ }
+ try x.rows(false, 0, 0xa000, 0, 2, 5, 0, 0);
+ try t.expectError(error.NonFiniteLogits, x.tokens(0, &out));
+ try t.expectEqual(@as(usize, 1), Fake.drawn);
+ picks[1] = .{ .token = 4, .nonfinite = 0 };
+ @memcpy(Fake.mem[64..][0..16], std.mem.sliceAsBytes(&picks));
+ try x.tokens(0, &out);
+ try t.expectEqualSlices(u32, &.{ 3, 4 }, &out);
+ cuda.copy_source.dropSpares(&d, cuda.copy_source.current());
+}
diff --git a/zig/src/cuda_dense/draws.zig b/zig/src/cuda_dense/draws.zig
new file mode 100644
index 00000000000..770d2cdd5be
--- /dev/null
+++ b/zig/src/cuda_dense/draws.zig
@@ -0,0 +1,106 @@
+//! The dense engine's draws: a ring of drawn tokens behind handles, greedy picks or keyed samples of bf16 logits.
+const std = @import("std");
+const cuda = @import("cuda");
+const kern = @import("kernels.zig");
+const mdl = @import("model.zig");
+
+const Pick = cuda.tree_round.Pick;
+const round_rows = mdl.round_rows;
+
+/// Drawn tokens a handle names (a ring; read before the round loop could wrap it).
+pub const ring = 1024;
+
+pub const Draws = struct {
+ d: *const cuda.Driver,
+ k: *const kern.Kernels,
+ s: cuda.Stream,
+ vocab: usize,
+ tok: cuda.DeviceBuffer, // drawn tokens (u32): the handle ring, then a round's rows
+ picks: cuda.DeviceBuffer, // argmax picks (Pick), the same entries
+ greedy: []bool, // entry e of the ring (and the round's rows) is a pick, not a draw
+ meta: cuda.DeviceBuffer, // a draw's first position minus one, a window each
+ next: u64 = 0,
+
+ /// The device bytes `init` allocates: the token and pick entries and the metas.
+ pub fn openBytes() usize {
+ return (ring + round_rows) * (4 + @sizeOf(Pick)) + round_rows * 16;
+ }
+
+ pub fn init(gpa: std.mem.Allocator, d: *const cuda.Driver, k: *const kern.Kernels, s: cuda.Stream, vocab: usize) !Draws {
+ const greedy = try gpa.alloc(bool, ring + round_rows);
+ errdefer gpa.free(greedy);
+ @memset(greedy, true);
+ var tok = try cuda.DeviceBuffer.alloc(d, (ring + round_rows) * 4);
+ errdefer tok.free();
+ var picks = try cuda.DeviceBuffer.alloc(d, (ring + round_rows) * @sizeOf(Pick));
+ errdefer picks.free();
+ const meta = try cuda.DeviceBuffer.alloc(d, round_rows * 16);
+ return .{ .d = d, .k = k, .s = s, .vocab = vocab, .tok = tok, .picks = picks, .greedy = greedy, .meta = meta };
+ }
+
+ pub fn deinit(x: *Draws, gpa: std.mem.Allocator) void {
+ gpa.free(x.greedy);
+ x.tok.free();
+ x.picks.free();
+ x.meta.free();
+ }
+
+ /// The next handle and its ring entry.
+ pub fn handle(x: *Draws) struct { h: u64, at: usize } {
+ const h = x.next;
+ x.next += 1;
+ return .{ .h = h, .at = @intCast(h % ring) };
+ }
+
+ /// Rows [row0, row0 + rows) of bf16 `logits` into entries [at, at + rows), row r at `position0 + r`.
+ pub fn rows(x: *Draws, sampled: bool, rule: u64, logits: u64, row0: usize, n: usize, position0: u64, at: usize, meta_at: usize) !void {
+ try x.pick(sampled, logits, row0, n, at);
+ if (!sampled) return;
+ try x.finite(at, n);
+ try x.draw(rule, logits, row0, n, position0, at, meta_at);
+ }
+
+ /// Every row's argmax pick into entries [at, at + n): a greedy row's token, and any row's NaN or infinity flag.
+ pub fn pick(x: *Draws, sampled: bool, logits: u64, row0: usize, n: usize, at: usize) !void {
+ const v = x.vocab;
+ @memset(x.greedy[at..][0..n], !sampled);
+ try x.k.round.argmax(x.s, logits + row0 * v * 2, x.picks.ptr + at * @sizeOf(Pick), n, v, v);
+ }
+
+ /// Entries [at, at + n)'s picks read back: a row of NaN or infinite logits is refused before any draw reads it.
+ pub fn finite(x: *Draws, at: usize, n: usize) !void {
+ try x.s.synchronize();
+ var picks: [round_rows]Pick = undefined;
+ try x.picks.download(at * @sizeOf(Pick), std.mem.sliceAsBytes(picks[0..n]));
+ for (picks[0..n]) |p| if (p.nonfinite != 0) return error.NonFiniteLogits;
+ }
+
+ /// Keyed samples of rows whose picks `finite` passed (tf_draw faults on a row with no finite logit).
+ pub fn draw(x: *Draws, rule: u64, logits: u64, row0: usize, n: usize, position0: u64, at: usize, meta_at: usize) !void {
+ const v = x.vocab;
+ const meta = [4]i32{ @intCast(position0 - 1), 0, 0, 0 };
+ const mp = x.meta.ptr + meta_at * 16;
+ try mdl.put(x.d, x.s, mp, std.mem.asBytes(&meta));
+ try x.k.draws.draw(x.s, logits + row0 * v * 2, v, rule, mp, 0, x.tok.ptr + at * 4, n, null, null);
+ }
+
+ /// Entries [at, at + out.len) as tokens after the stream's work; non-finite logits are an error, drawn or picked.
+ pub fn tokens(x: *Draws, at: usize, out: []u32) !void {
+ try x.s.synchronize();
+ var picks: [round_rows]Pick = undefined;
+ try x.tok.download(at * 4, std.mem.sliceAsBytes(out));
+ try x.picks.download(at * @sizeOf(Pick), std.mem.sliceAsBytes(picks[0..out.len]));
+ for (out, picks[0..out.len], x.greedy[at..][0..out.len]) |*t, p, g| {
+ if (p.nonfinite != 0) return error.NonFiniteLogits;
+ if (g) t.* = p.token;
+ }
+ }
+
+ /// The token handle `h` names, once drawn; a handle the ring has passed is refused.
+ pub fn read(x: *Draws, h: u64) !u32 {
+ if (h >= x.next or x.next - h > ring) return error.NoSuchToken;
+ var out: [1]u32 = undefined;
+ try x.tokens(@intCast(h % ring), &out);
+ return out[0];
+ }
+};
diff --git a/zig/src/cuda_dense/kernels.zig b/zig/src/cuda_dense/kernels.zig
new file mode 100644
index 00000000000..32365e5cc62
--- /dev/null
+++ b/zig/src/cuda_dense/kernels.zig
@@ -0,0 +1,145 @@
+//! The dense engine's shared launches: the lane matmul, norms, embedding rows, bias, draws, paged attention, SwiGLU.
+const std = @import("std");
+const cuda = @import("cuda");
+const recipe = @import("recipe.zig");
+const pages = @import("pages.zig");
+
+const Args = cuda.Args;
+const Function = cuda.Function;
+const Stream = cuda.Stream;
+const Geometry = recipe.Geometry;
+
+/// Streams one shared-window attention launch takes (paged_attn.cuh's MAXSEG).
+pub const max_segs = 8;
+
+/// paged_attn.cuh's AttnSegs: each stream's caches, position, page table and first row.
+pub const AttnSegs = extern struct { kc: [max_segs]u64, vc: [max_segs]u64, meta: [max_segs]u64, pt: [max_segs]u64, ps: i64, row0: [max_segs]c_int, n: c_int };
+
+const images = .{ "sample", "tree_round", "prefill_attention", "nemotron_attention", "lfm2" };
+
+fn int(x: usize) c_int {
+ return @intCast(x);
+}
+
+pub const Kernels = struct {
+ mods: [images.len]cuda.Module,
+ g: Geometry,
+ shape: recipe.Shape,
+ lane: cuda.qmmf.Lane,
+ norms: cuda.norms.Norms,
+ embed: cuda.embed_rows.EmbedRows,
+ vit: cuda.vit.Kernels,
+ draws: cuda.sample.Draws,
+ round: cuda.tree_round.Kernels,
+ pattn: Function,
+ kv_write: Function,
+ chunk: Function,
+ merge: Function,
+ silu_mul: Function,
+
+ /// The shared modules, and the attention launches `g`'s shape names; a geometry the kernels lack refuses by name.
+ pub fn load(ctx: *const cuda.Context, g: Geometry) !Kernels {
+ const d = ctx.d;
+ const shape = try recipe.attentionShape(g);
+ if (!cuda.kernels.available) return error.BuiltWithoutKernels;
+ var k: Kernels = undefined;
+ k.g = g;
+ k.shape = shape;
+ var loaded: usize = 0;
+ errdefer for (k.mods[0..loaded]) |*m| m.unload();
+ inline for (images, 0..) |name, i| {
+ k.mods[i] = try cuda.Module.load(d, @field(cuda.kernels, name));
+ loaded += 1;
+ }
+ const m = &k.mods;
+ k.draws = try cuda.sample.Draws.resolve(m[0]);
+ k.round = try cuda.tree_round.Kernels.resolve(m[1]);
+ k.pattn = try m[2].function(shape.pattn);
+ try k.pattn.allowDynamicShared(shape.shared);
+ k.kv_write = try m[3].function("tf_nemo_kv_write_segs");
+ k.chunk = try m[3].function(shape.chunk);
+ k.merge = try m[3].function(shape.merge);
+ k.silu_mul = try m[4].function("tf_lfm2_silu_mul");
+ k.lane = try cuda.qmmf.Lane.load(d, (try ctx.capability()) / 10);
+ errdefer k.lane.unload();
+ k.norms = try cuda.norms.Norms.load(d);
+ errdefer k.norms.unload();
+ k.embed = try cuda.embed_rows.EmbedRows.load(d);
+ errdefer k.embed.unload();
+ k.vit = try cuda.vit.Kernels.load(d);
+ return k;
+ }
+
+ pub fn deinit(k: *Kernels) void {
+ k.vit.unload();
+ k.embed.unload();
+ k.norms.unload();
+ k.lane.unload();
+ for (&k.mods) |*m| m.unload();
+ }
+
+ fn go(f: Function, s: Stream, grid: [3]usize, block: usize, smem: u32, a: *Args) !void {
+ const g: cuda.Dim3 = .{ .x = @intCast(grid[0]), .y = @intCast(grid[1]), .z = @intCast(grid[2]) };
+ try cuda.launch.launch(f, .{ .grid = g, .block = .{ .x = @intCast(block) }, .shared = smem }, s, a);
+ }
+
+ /// out [rows, f] = bf16(bf16(silu(gate)) * up) from rows gate | up [rows, 2 f] (lfm2.cu).
+ pub fn siluMul(k: *const Kernels, s: Stream, gu: u64, out: u64, rows: usize, f: usize) !void {
+ var a: Args = .{};
+ a.add(gu);
+ a.add(out);
+ a.add(int(f));
+ try go(k.silu_mul, s, .{ rows, (f + 255) / 256, 1 }, 256, 0, &a);
+ }
+
+ /// Rows k, v (kv [rows, 2 kv width]) into each stream's pages at its position on.
+ pub fn kvWrite(k: *const Kernels, s: Stream, sg: AttnSegs, kv: u64, rows: usize) !void {
+ const kvw = k.g.kvWidth();
+ var a: Args = .{};
+ a.add(sg);
+ a.add(kv);
+ a.add(int(2 * kvw));
+ a.add(@as(c_int, 0));
+ a.add(int(kvw));
+ try go(k.kv_write, s, .{ rows, 1, 1 }, 256, 0, &a);
+ }
+
+ fn scale(k: *const Kernels) f32 {
+ return 1.0 / @sqrt(@as(f32, @floatFromInt(k.g.head_dim)));
+ }
+
+ /// Decode rows: q [rows, heads, dim] over each stream's pages in 512-key chunks, merged in order into out and xs.
+ pub fn attend(k: *const Kernels, s: Stream, sg: AttnSegs, q: u64, po: u64, pm: u64, pl: u64, out: u64, xs: u64, rows: usize, nch: usize, z: usize) !void {
+ const g = k.g;
+ var a: Args = .{};
+ a.add(sg);
+ a.add(q);
+ for ([_]u64{ po, pm, pl }) |v| a.add(v);
+ for ([_]usize{ g.qWidth(), g.heads, g.kv_heads, g.group(), pages.page_rows, nch }) |v| a.add(int(v));
+ a.add(k.scale());
+ try go(k.chunk, s, .{ rows, g.kv_heads, z }, 256, 0, &a);
+ var b: Args = .{};
+ b.add(sg);
+ for ([_]u64{ po, pm, pl, out, xs }) |v| b.add(v);
+ for ([_]usize{ g.heads, g.kv_heads, g.group(), pages.page_rows, nch }) |v| b.add(int(v));
+ try go(k.merge, s, .{ rows, g.kv_heads, 1 }, 256, 0, &b);
+ }
+
+ /// A prompt chunk's rows at positions p0.. against its stream's pages (written through them): out [rows, H, dim].
+ pub fn prompt(k: *const Kernels, s: Stream, q: u64, kc: u64, vc: u64, out: u64, p0: usize, rows: usize, pt: u64, ps: usize) !void {
+ const g = k.g;
+ var a: Args = .{};
+ for ([_]u64{ q, kc, vc, out }) |v| a.add(v);
+ for ([_]usize{ p0, rows, g.heads, g.kv_heads, g.group() }) |v| a.add(int(v));
+ a.add(k.scale());
+ a.add(pt);
+ a.add(@as(i64, @intCast(ps)));
+ try go(k.pattn, s, .{ k.shape.blocks(rows), k.shape.grid_y, 1 }, 256, k.shape.shared, &a);
+ }
+};
+
+test "AttnSegs is paged_attn.cuh's struct, padded to 8 bytes: 304" {
+ try std.testing.expectEqual(@as(usize, 304), @sizeOf(AttnSegs));
+ try std.testing.expectEqual(@as(usize, 256), @offsetOf(AttnSegs, "ps"));
+ try std.testing.expectEqual(@as(usize, 296), @offsetOf(AttnSegs, "n"));
+}
diff --git a/zig/src/cuda_dense/model.zig b/zig/src/cuda_dense/model.zig
new file mode 100644
index 00000000000..06a293223cf
--- /dev/null
+++ b/zig/src/cuda_dense/model.zig
@@ -0,0 +1,251 @@
+//! The dense engine's device state: the forward's scratch, the page arena of every layer's KV, each slot's table.
+const std = @import("std");
+const cuda = @import("cuda");
+const kv = @import("core").kv_pool;
+const recipe = @import("recipe.zig");
+const pages = @import("pages.zig");
+const rope = @import("rope.zig");
+
+const DeviceBuffer = cuda.DeviceBuffer;
+const Geometry = recipe.Geometry;
+
+/// Rows a prompt chunk takes.
+pub const prompt_rows = 1024;
+/// Rows a round of windows takes over every stream.
+pub const round_rows = 128;
+/// Key chunks one decode attention launch walks in parallel at most.
+const chunk_blocks = 64;
+
+/// The forward's buffers, `R` rows of each (a prompt chunk's); a recipe reads and writes them through its Pass.
+pub const Scratch = struct {
+ ids: u64, // int32 [R]: tokens
+ pos: u64, // int32 [R]: each row's position
+ meta: u64, // int32 [round_rows]: each chain's first position (AttnSegs' META)
+ h: u64, // bf16 [R, hidden]: the residual
+ x: u64, // bf16 [R, hidden]: a norm's output
+ r: u64, // bf16 [R, hidden]: a block's output
+ acc: u64, // fp32 [R, acc width]: a projection's sums before its bias
+ wide: u64, // bf16 [R, wide]: gate|up
+ y: u64, // bf16 [R, max(q width, mid)]: attention out, the MLP's activation
+ q: u64, // bf16 [R, q width]: rotated queries
+ kv: u64, // bf16 [R, 2 kv width]: keys then values a row
+ xs: u64, // fp32 [round_rows, q width / 64]: decode attention's 64-value sums
+ po: u64, // decode attention partials: o, m, l
+ pm: u64,
+ pl: u64,
+ logits: u64, // bf16 [round_rows, vocab]
+ part: u64, // the lane matmul's slice partials
+};
+
+/// bytes to dst on `s`: a private pinned copy kept until the stream completes, so the source may change at once.
+pub fn put(d: *const cuda.Driver, s: cuda.Stream, dst: u64, bytes: []const u8) !void {
+ try cuda.copy_source.enqueue(d, dst, bytes, s);
+}
+
+pub const Model = struct {
+ gpa: std.mem.Allocator,
+ d: *const cuda.Driver,
+ g: Geometry,
+ context: usize, // the served window: a request's prompt and reply fit in it
+ slots: usize = 0, // streams with a table row: none until attach
+ pages: usize = 0, // pages in the arena
+ arena: u64 = 0, // [page][layer][K, V][512][kv width] bf16
+ table: u64 = 0, // int32 [slots][perSlot()]: slot s's page ids in position order
+ ids: []u32 = &.{}, // the tables on the host
+ caps: []usize = &.{}, // positions slot s's pages cover
+ inv: u64 = 0, // fp32 [head_dim / 2]: RoPE's inverse frequencies (a geometry with theta)
+ acc_width: usize,
+ part_bytes: usize,
+ s: Scratch = undefined,
+ buffers: std.ArrayList(DeviceBuffer) = .empty,
+ planned: ?usize = null, // a plan: the bytes counted, nothing allocated
+
+ pub fn perSlot(m: *const Model) usize {
+ return pages.perSlot(m.context);
+ }
+
+ /// Positions a slot's table reaches: the window and its verify rows, in whole pages.
+ pub fn capacity(m: *const Model) usize {
+ return m.perSlot() * pages.page_rows;
+ }
+
+ pub fn pageElems(m: *const Model) usize {
+ return pages.pageElems(m.g);
+ }
+
+ pub fn pageBytes(m: *const Model) u64 {
+ return pages.pageBytes(m.g);
+ }
+
+ pub fn layout(m: *const Model) kv.Layout {
+ return pages.layout(m.g, m.context);
+ }
+
+ pub fn streamBytes(m: *const Model) u64 {
+ return pages.streamBytes(m.g, m.context);
+ }
+
+ /// Decode attention's key chunks a row walks at most, and the blocks that walk them.
+ pub fn chunks(m: *const Model) usize {
+ return m.perSlot();
+ }
+
+ pub fn chunkBlocks(m: *const Model) usize {
+ return @min(m.chunks(), chunk_blocks);
+ }
+
+ fn alloc(m: *Model, bytes: usize) !u64 {
+ if (m.planned) |*p| {
+ p.* += (@max(bytes, 256) + 255) / 256 * 256;
+ return 0;
+ }
+ const b = try DeviceBuffer.alloc(m.d, @max(bytes, 256));
+ errdefer {
+ var x = b;
+ x.free();
+ }
+ try m.buffers.append(m.gpa, b);
+ return b.ptr;
+ }
+
+ /// The f32 sums width every biased projection fits, and the lane's largest slice partials over the shapes.
+ fn sizes(g: Geometry, lane: *const cuda.qmmf.Lane) struct { acc: usize, part: usize } {
+ const shapes = [_][2]usize{ .{ g.acc, g.hidden }, .{ g.hidden, g.qWidth() }, .{ g.wide, g.hidden }, .{ g.hidden, g.mid }, .{ g.vocab, g.hidden } };
+ var part: usize = 0;
+ for (shapes) |nk| for (1..prompt_rows + 1) |rows| {
+ part = @max(part, lane.partBytes(rows, cuda.modelopt.bf16Weight(0, nk[0], nk[1])));
+ };
+ return .{ .acc = @max(g.acc, g.wide, g.hidden), .part = part };
+ }
+
+ fn blank(gpa: std.mem.Allocator, d: *const cuda.Driver, g: Geometry, context: usize, lane: *const cuda.qmmf.Lane) Model {
+ const z = sizes(g, lane);
+ return .{ .gpa = gpa, .d = d, .g = g, .context = context, .acc_width = z.acc, .part_bytes = z.part };
+ }
+
+ /// The scratch and the RoPE table; the arena and tables come with attach, once the host has planned the pages.
+ pub fn init(gpa: std.mem.Allocator, d: *const cuda.Driver, s: cuda.Stream, g: Geometry, context: usize, lane: *const cuda.qmmf.Lane) !Model {
+ var m = blank(gpa, d, g, context, lane);
+ errdefer m.deinit();
+ errdefer cuda.completion.drain(d, s, "the dense engine's scratch uploads did not drain before their buffers were freed");
+ try m.build(s);
+ return m;
+ }
+
+ /// The device bytes `init` allocates, counted by the same path without allocating.
+ pub fn openBytes(gpa: std.mem.Allocator, d: *const cuda.Driver, s: cuda.Stream, g: Geometry, context: usize, lane: *const cuda.qmmf.Lane) !usize {
+ var m = blank(gpa, d, g, context, lane);
+ m.planned = 0;
+ defer m.deinit();
+ try m.build(s);
+ return m.planned.?;
+ }
+
+ fn build(m: *Model, s: cuda.Stream) !void {
+ const g = m.g;
+ const r = prompt_rows;
+ const parts = round_rows * m.chunks() * g.heads;
+ m.s = .{
+ .ids = try m.alloc(r * 4),
+ .pos = try m.alloc(r * 4),
+ .meta = try m.alloc(round_rows * 4),
+ .h = try m.alloc(r * g.hidden * 2),
+ .x = try m.alloc(r * g.hidden * 2),
+ .r = try m.alloc(r * g.hidden * 2),
+ .acc = try m.alloc(r * m.acc_width * 4),
+ .wide = try m.alloc(r * g.wide * 2),
+ .y = try m.alloc(r * @max(g.qWidth(), g.mid) * 2),
+ .q = try m.alloc(r * g.qWidth() * 2),
+ .kv = try m.alloc(r * 2 * g.kvWidth() * 2),
+ .xs = try m.alloc(round_rows * (g.qWidth() / 64) * 4),
+ .po = try m.alloc(parts * g.head_dim * 4),
+ .pm = try m.alloc(parts * 4),
+ .pl = try m.alloc(parts * 4),
+ .logits = try m.alloc(round_rows * g.vocab * 2),
+ .part = try m.alloc(m.part_bytes),
+ };
+ const theta = g.theta orelse return;
+ m.inv = try m.alloc(rope.count(g.head_dim) * 4);
+ if (m.planned != null) return;
+ const inv = try m.gpa.alloc(f32, rope.count(g.head_dim));
+ defer m.gpa.free(inv);
+ if (g.inv_freq) |table| {
+ if (table.len != inv.len) return error.BadRopeTable;
+ @memcpy(inv, table);
+ } else rope.inverse(theta, g.head_dim, inv);
+ try m.upload(s, m.inv, std.mem.sliceAsBytes(inv));
+ try s.synchronize();
+ }
+
+ fn upload(m: *const Model, s: cuda.Stream, dst: u64, bytes: []const u8) !void {
+ try put(m.d, s, dst, bytes);
+ }
+
+ pub fn deinit(m: *Model) void {
+ for (m.buffers.items) |*b| b.free();
+ m.buffers.deinit(m.gpa);
+ m.gpa.free(m.ids);
+ m.gpa.free(m.caps);
+ }
+
+ /// The arena of `pool` pages (zeroed) and the tables of `extra + 1` slots, no page mapped.
+ pub fn attach(m: *Model, s: cuda.Stream, extra: usize, pool: usize) !void {
+ if (m.pages != 0 or pool == 0 or extra >= round_rows or m.planned != null) return error.BadAttach;
+ const mark = m.buffers.items.len; // the errdefer reads it at failure, when the list has grown
+ errdefer m.undo(s, mark);
+ const bytes = pool * m.pageBytes();
+ m.arena = try m.alloc(bytes);
+ try m.d.check(m.d.api.cuMemsetD8Async(m.arena, 0, bytes, s.handle), "cuMemsetD8Async");
+ const slots = extra + 1;
+ m.table = try m.alloc(slots * m.perSlot() * 4);
+ m.ids = try m.gpa.alloc(u32, slots * m.perSlot());
+ @memset(m.ids, 0);
+ m.caps = try m.gpa.alloc(usize, slots);
+ @memset(m.caps, 0);
+ try m.upload(s, m.table, std.mem.sliceAsBytes(m.ids));
+ try s.synchronize();
+ m.slots = slots;
+ m.pages = pool;
+ }
+
+ /// A failed attach: its fill and table upload proved complete, then its arena, tables and host rows gone.
+ fn undo(m: *Model, s: cuda.Stream, mark: usize) void {
+ cuda.completion.drain(m.d, s, "the dense engine's page arena fill did not drain before it was freed");
+ while (m.buffers.items.len > mark) {
+ var b = m.buffers.pop().?;
+ b.free();
+ }
+ m.gpa.free(m.ids);
+ m.gpa.free(m.caps);
+ m.ids = &.{};
+ m.caps = &.{};
+ m.arena = 0;
+ m.table = 0;
+ }
+
+ /// Slot `slot`'s pages from table entry `from` on are `ids` (arena page numbers in position order).
+ pub fn mapPages(m: *Model, s: cuda.Stream, slot: usize, ids: []const u32, from: usize) !void {
+ const per = m.perSlot();
+ if (slot >= m.slots or from + ids.len > per) return error.BadChain;
+ for (ids) |id| if (id >= m.pages) return error.BadChain;
+ const at = slot * per + from;
+ @memcpy(m.ids[at..][0..ids.len], ids);
+ m.caps[slot] = (from + ids.len) * pages.page_rows;
+ try m.upload(s, m.table + at * 4, std.mem.sliceAsBytes(m.ids[at..][0..ids.len]));
+ }
+
+ /// Slot `slot` holds no pages: its table entries stay, no forward may reach them.
+ pub fn unmapPages(m: *Model, slot: usize) void {
+ if (slot < m.slots) m.caps[slot] = 0;
+ }
+
+ /// Layer `layer`'s key (or value) cache as the kernels take it: its part of page 0.
+ pub fn cache(m: *const Model, layer: usize, value: bool) u64 {
+ return m.arena + ((2 * layer + @intFromBool(value)) * pages.page_rows * m.g.kvWidth()) * 2;
+ }
+
+ /// Slot s's page table row.
+ pub fn tableOf(m: *const Model, slot: usize) u64 {
+ return m.table + slot * m.perSlot() * 4;
+ }
+};
diff --git a/zig/src/cuda_dense/native.zig b/zig/src/cuda_dense/native.zig
new file mode 100644
index 00000000000..818ec0f2bf9
--- /dev/null
+++ b/zig/src/cuda_dense/native.zig
@@ -0,0 +1,152 @@
+//! Native(R): a dense recipe as the native CUDA host's family, its decls generated once for every recipe.
+const std = @import("std");
+const cuda = @import("cuda");
+const lanes = @import("lanes");
+const kv = @import("core").kv_pool;
+const recipe = @import("recipe.zig");
+const kern = @import("kernels.zig");
+const mdl = @import("model.zig");
+const pages = @import("pages.zig");
+const backend = @import("backend.zig");
+
+/// The refusals the engine words itself, for every recipe; a recipe's own come from its `explain`.
+pub const engine_errors = [_]anyerror{ error.PastContext, error.NoFreeSlot, error.NoDraftHead, error.DenseTextOnly, error.StructuresUnsupported, error.NonFiniteLogits, error.CapturedKernels, error.ParallelPastRound, error.NoStreamFits, error.UnsupportedDenseHeadDim, error.UnsupportedDenseGroup };
+
+pub fn Native(comptime R: type) type {
+ comptime recipe.check(R);
+ return struct {
+ const Dense = backend.Dense(R);
+
+ pub const model_type: []const u8 = R.model_type;
+ pub const formats: []const []const u8 = R.formats;
+ pub const default_context: i64 = R.default_context;
+ pub const max_segments: u32 = 1;
+ pub const prompt_rows: u32 = backend.prompt_rows;
+ /// A request's verify reserve: its pages hold that many rows past its prompt and reply.
+ pub const window_rows: u32 = backend.max_window;
+ /// `explain` words this family's open refusals too.
+ pub const explains_open = true;
+ /// No drafter: --drafter is refused before the load.
+ pub const takes_drafter = false;
+ pub const drafter_refusal = std.fmt.comptimePrint("--drafter: {s} on CUDA loads no drafter; serve it without --drafter", .{R.name});
+ /// No prompt states: the host plans no prompt cache for this family and refuses --prompt-cache-gib.
+ pub const keeps_no_prompts = true;
+ /// Whether replies open with a reasoning block (the recipe's word, else the host's default).
+ pub const reasoning: bool = if (@hasDecl(R, "reasoning")) R.reasoning else true;
+
+ /// The server's reading of the room left after the weights; null if unreadable.
+ pub const Room = struct { ctx: *anyopaque, read: *const fn (*anyopaque) ?u64 };
+ /// No vision, precision, kv8, prompt_int8, loops or rank field: the host refuses those flags by name.
+ pub const Options = struct { context: usize, drafts: bool, segments: usize = 1, slide: bool = false, lanes: u32 = 8, lanes_fixed: bool = false, room: ?Room = null };
+
+ pub const Loaded = struct {
+ backend: lanes.backend.Backend,
+ facts: lanes.Model,
+ rows: u32,
+ stream_bytes: usize,
+ free_streams: u32 = 1,
+ ctx: *anyopaque,
+ deinit: *const fn (*anyopaque) void,
+ target: *Dense,
+ /// Every stream's keys and values are pages of the core pool, the first stream's too.
+ layout: ?kv.Layout,
+ engine_paged: bool = true,
+ };
+
+ const Owned = struct { gpa: std.mem.Allocator, s: cuda.Stream, w: R.Weights, k: kern.Kernels, own: R.Kernels, m: mdl.Model, b: Dense };
+
+ /// The checkpoint, the shared and the recipe's kernels, the scratch; streams' pages come with attach.
+ pub fn open(gpa: std.mem.Allocator, io: std.Io, ctx: *const cuda.Context, dir: []const u8, kernels: ?[]const u8, o: Options) !Loaded {
+ if (kernels != null) return error.CapturedKernels;
+ if (o.lanes_fixed and o.lanes > backend.round_rows) return error.ParallelPastRound;
+ const g = try R.geometry(gpa, io, dir);
+ _ = try recipe.attentionShape(g); // refused by name before the weights load
+ const own = try gpa.create(Owned);
+ errdefer gpa.destroy(own);
+ own.gpa = gpa;
+ own.s = try cuda.Stream.init(ctx.d, false);
+ errdefer own.s.deinit();
+ own.w = try R.load(gpa, io, ctx.d, own.s, dir);
+ errdefer own.w.deinit();
+ own.k = try kern.Kernels.load(ctx, g);
+ errdefer own.k.deinit();
+ own.own = try R.Kernels.load(ctx);
+ errdefer own.own.deinit();
+ const room: u64 = if (o.room) |r| r.read(r.ctx) orelse 0 else std.math.maxInt(u64);
+ const bytes = try mdl.Model.openBytes(gpa, ctx.d, own.s, g, o.context, &own.k.lane) + Dense.openBytes(g.vocab);
+ if (bytes > room) return error.NoStreamFits;
+ own.m = try mdl.Model.init(gpa, ctx.d, own.s, g, o.context, &own.k.lane);
+ errdefer own.m.deinit();
+ own.b = try Dense.init(gpa, ctx.d, &own.m, &own.k, &own.own, &own.w, own.s);
+ return .{
+ .backend = own.b.backend(),
+ .facts = own.b.facts(),
+ .rows = backend.max_window,
+ .stream_bytes = own.m.streamBytes(),
+ .ctx = own,
+ .deinit = release,
+ .target = &own.b,
+ .layout = own.m.layout(),
+ };
+ }
+
+ /// The checkpoint's weight bytes, counted from its tensors before the load.
+ pub fn textWeightBytes(a: std.mem.Allocator, io: std.Io, dir: []const u8) !?u64 {
+ return try R.weightBytes(a, io, dir);
+ }
+
+ fn owned(p: *anyopaque) *Owned {
+ return @ptrCast(@alignCast(p));
+ }
+
+ /// The pool: `streams` more slots beside the first and an arena of `n` pages.
+ pub fn attach(p: *anyopaque, streams: u32, n: usize) !void {
+ try owned(p).b.attach(streams, n);
+ }
+
+ /// Whether a request's pages could be promised now (the lane host's admission, on its engine thread).
+ pub fn fits(p: *anyopaque, prompt: u32, max_tokens: u32) bool {
+ return owned(p).b.admits(prompt, max_tokens);
+ }
+
+ /// The pool the lane host's ledger admits this family's promises by.
+ pub fn ledgerPool(p: *anyopaque) ?*kv.Pool {
+ const b = &owned(p).b;
+ return if (b.pool) |*pool| pool else null;
+ }
+
+ /// A request or load this engine refuses, in words; then the recipe's own; null: none.
+ pub fn explain(_: ?*anyopaque, err: anyerror) ?[]const u8 {
+ return words(err) orelse if (@hasDecl(R, "explain")) R.explain(err) else null;
+ }
+
+ fn words(err: anyerror) ?[]const u8 {
+ const name = R.name;
+ return switch (err) {
+ error.PastContext => "the prompt and its reply exceed this server's context window: shorten it or lower max_tokens",
+ error.NoFreeSlot => "every stream's caches are in use: retry once another request ends",
+ error.NoDraftHead => name ++ " on CUDA has no draft head: it drafts from the prompt's own text",
+ error.DenseTextOnly => name ++ " on CUDA reads text prompts only so far: this request has images",
+ error.StructuresUnsupported => "structured output is not built for " ++ name ++ " on CUDA yet",
+ error.NonFiniteLogits => "the model's logits went non-finite at this row: the request is stopped rather than answered from them",
+ error.CapturedKernels => name ++ " runs on the engine's own kernels: unset TENSORFOLD_CUDA_KERNELS",
+ error.ParallelPastRound => std.fmt.comptimePrint("--parallel: {s} on CUDA holds at most {d} streams (a round's rows): serve --parallel {d} or fewer, or auto", .{ name, backend.round_rows, backend.round_rows }),
+ error.NoStreamFits => "the CUDA memory budget fits no " ++ name ++ " stream after the weights: lower --context, free device memory, or adjust TENSORFOLD_MEMORY_RESERVE_GIB / TENSORFOLD_CUDA_MEMORY_LIMIT_GB",
+ error.UnsupportedDenseHeadDim => "this " ++ name ++ " checkpoint's attention head dim is not 64 or 128, the two the CUDA dense engine's attention kernels take",
+ error.UnsupportedDenseGroup => "this " ++ name ++ " checkpoint's query heads a KV head are not an even count of at most 16, the groups the CUDA dense engine's attention kernels take",
+ else => null,
+ };
+ }
+
+ fn release(p: *anyopaque) void {
+ const own = owned(p);
+ own.b.deinit();
+ own.m.deinit();
+ own.own.deinit();
+ own.k.deinit();
+ own.w.deinit();
+ own.s.deinit();
+ own.gpa.destroy(own);
+ }
+ };
+}
diff --git a/zig/src/cuda_dense/native_test.zig b/zig/src/cuda_dense/native_test.zig
new file mode 100644
index 00000000000..6788f5675a0
--- /dev/null
+++ b/zig/src/cuda_dense/native_test.zig
@@ -0,0 +1,105 @@
+//! Native(R)'s host contract with a tiny recipe: the decls it emits and omits, its refusals' words, a compiled path.
+const std = @import("std");
+const cuda = @import("cuda");
+const native = @import("native.zig");
+const Pass = @import("pass.zig").Pass;
+const Geometry = @import("recipe.zig").Geometry;
+const Allocator = std.mem.Allocator;
+
+const tiny: Geometry = .{ .hidden = 256, .layers = 2, .heads = 4, .kv_heads = 2, .head_dim = 64, .vocab = 512, .eps = 1e-6, .context = 4096, .acc = 512, .wide = 1024, .mid = 512, .theta = 1e4 };
+
+/// A recipe of the dots shape on tiny sizes: biased q|k|v sums, attention, o, SwiGLU, an untied head.
+const Tiny = struct {
+ pub const name = "Tiny";
+ pub const model_type = "tiny_dense";
+ pub const formats: []const []const u8 = &.{"bf16"};
+ pub const default_context: i64 = 4096;
+ pub const reasoning = false;
+ pub const post_embed: ?fn (*const Pass(Tiny)) anyerror!void = null;
+ pub const Weights = struct {
+ embed: u64 = 0,
+ norm: u64 = 0,
+ bias: u64 = 0,
+ proj: cuda.qmmf.Weight = cuda.modelopt.bf16Weight(0, 512, 256),
+ pub fn deinit(_: *Weights) void {}
+ };
+ pub const Kernels = struct {
+ pub fn load(_: *const cuda.Context) !Kernels {
+ return .{};
+ }
+ pub fn deinit(_: *Kernels) void {}
+ };
+ pub fn geometry(_: Allocator, _: std.Io, _: []const u8) !Geometry {
+ return tiny;
+ }
+ pub fn load(_: Allocator, _: std.Io, _: *const cuda.Driver, _: cuda.Stream, _: []const u8) !Weights {
+ return .{};
+ }
+ pub fn weightBytes(_: Allocator, _: std.Io, _: []const u8) !u64 {
+ return 1 << 20;
+ }
+ pub fn embed(p: *const Pass(Tiny)) !void {
+ try p.k.embed.bf16(p.s, p.m.s.ids, p.w.embed, p.m.s.h, p.rows, p.g.hidden);
+ }
+ pub fn layer(p: *const Pass(Tiny), i: usize) !void {
+ const x = p.m.s;
+ try p.norm(if (i == 0) null else x.r, p.w.norm);
+ try p.projectBias(p.w.proj, p.w.bias, x.x, x.wide);
+ try p.projectF32(p.w.proj, x.x, x.acc);
+ try p.attend(i, x.q, x.kv, x.y);
+ try p.project(p.w.proj, x.y, x.r);
+ try p.siluMul(x.wide, x.y, p.g.mid);
+ }
+ pub fn final(p: *const Pass(Tiny)) !void {
+ try p.norm(p.m.s.r, p.w.norm);
+ }
+ pub fn head(w: *const Weights) cuda.qmmf.Weight {
+ return w.proj;
+ }
+ pub fn explain(err: anyerror) ?[]const u8 {
+ return if (err == error.TinyConfig) "this Tiny checkpoint is not the one the recipe serves" else null;
+ }
+};
+
+const N = native.Native(Tiny);
+
+test "Native emits the host decls a dense family takes and no others" {
+ inline for (.{ "model_type", "formats", "default_context", "max_segments", "prompt_rows", "window_rows", "explains_open", "takes_drafter", "drafter_refusal", "Room", "Options", "Loaded", "open", "textWeightBytes", "attach", "fits", "ledgerPool", "explain", "keeps_no_prompts", "reasoning" }) |d| {
+ if (!@hasDecl(N, d)) return error.MissingDecl;
+ }
+ // each of these switches a host path on (prompt cache, KV sharing, learning, own planning, drafters, ranks)
+ inline for (.{ "snap_save", "snap_restore", "snap_drop", "snap_bytes", "snap_slots", "snap_grid", "snap_lookahead", "share_attach", "share_pool", "share_states", "kvPool", "slide", "learnModel", "linked", "contextCeiling", "pagedLayout", "rankRoom", "request_starts", "request_starts_switch", "openDrafter", "warm_quiet" }) |d| {
+ if (@hasDecl(N, d)) return error.UnexpectedDecl;
+ }
+ inline for (.{ "context", "drafts", "segments", "slide", "lanes", "lanes_fixed", "room" }) |f| try std.testing.expect(@hasField(N.Options, f));
+ inline for (.{ "vision", "precision", "kv8", "prompt_int8", "loops", "rank", "drafter" }) |f| try std.testing.expect(!@hasField(N.Options, f));
+ inline for (.{ "layout", "engine_paged", "free_streams", "target" }) |f| try std.testing.expect(@hasField(N.Loaded, f));
+ try std.testing.expectEqualStrings("tiny_dense", N.model_type);
+ try std.testing.expect(N.keeps_no_prompts and !N.reasoning and !N.takes_drafter);
+ try std.testing.expectEqual(@as(u32, 16), N.window_rows);
+ try std.testing.expectEqual(@as(u32, 1024), N.prompt_rows);
+ try std.testing.expectEqual(@as(u32, 1), N.max_segments);
+ try std.testing.expectStringStartsWith(N.drafter_refusal, "--drafter: Tiny on CUDA");
+}
+
+test "every engine refusal is worded, the recipe's own after it, the rest left to the server" {
+ for (native.engine_errors) |e| try std.testing.expect(N.explain(null, e) != null);
+ try std.testing.expectStringStartsWith(N.explain(null, error.TinyConfig).?, "this Tiny checkpoint");
+ try std.testing.expectStringStartsWith(N.explain(null, error.DenseTextOnly).?, "Tiny on CUDA reads text prompts only");
+ try std.testing.expect(N.explain(null, error.OutOfMemory) == null);
+}
+
+test "open refuses captured kernels and a fixed --parallel past a round before it reads the checkpoint" {
+ const ctx: *const cuda.Context = undefined;
+ const base: N.Options = .{ .context = 4096, .drafts = true };
+ try std.testing.expectError(error.CapturedKernels, N.open(std.testing.allocator, undefined, ctx, "", "captured", base));
+ var wide = base;
+ wide.lanes = 129;
+ wide.lanes_fixed = true;
+ try std.testing.expectError(error.ParallelPastRound, N.open(std.testing.allocator, undefined, ctx, "", null, wide));
+}
+
+test "a recipe's whole path compiles against the engine: open, the lane backend, the forward" {
+ const hooks = .{ &N.open, &N.attach, &N.fits, &N.ledgerPool, &N.textWeightBytes, &N.explain };
+ inline for (hooks) |h| try std.testing.expect(@intFromPtr(h) != 0);
+}
diff --git a/zig/src/cuda_dense/pages.zig b/zig/src/cuda_dense/pages.zig
new file mode 100644
index 00000000000..83698cae7ea
--- /dev/null
+++ b/zig/src/cuda_dense/pages.zig
@@ -0,0 +1,66 @@
+//! KV pages of the dense engine: 512 rows of every layer's keys and values, the host pool's layout, a request's pages.
+const std = @import("std");
+const kv = @import("core").kv_pool;
+const Geometry = @import("recipe.zig").Geometry;
+
+/// Rows a page holds: one decode attention chunk (nemotron_attention.cu's PAGE).
+pub const page_rows = 512;
+/// The widest window a stream verifies: a request's pages hold that many rows past its prompt and reply.
+pub const window = 16;
+
+/// Pages that hold positions [0, positions).
+pub fn pagesOf(positions: usize) usize {
+ return (positions + page_rows - 1) / page_rows;
+}
+
+/// Pages a request holds at most: its prompt, its reply and the verify window.
+pub fn pagesFor(prompt: usize, max_new: usize) usize {
+ return pagesOf(prompt + max_new + window);
+}
+
+/// Table entries a stream takes at a `context`-token window, its verify window included.
+pub fn perSlot(context: usize) usize {
+ return pagesOf(context + window);
+}
+
+/// bf16 elements a page holds: [layer][K, V][512][kv width].
+pub fn pageElems(g: Geometry) usize {
+ return g.layers * 2 * page_rows * g.kvWidth();
+}
+
+pub fn pageBytes(g: Geometry) u64 {
+ return pageElems(g) * 2;
+}
+
+/// Bytes of one stream at a whole `context`-token window: its pages and its table row.
+pub fn streamBytes(g: Geometry, context: usize) u64 {
+ return perSlot(context) * (pageBytes(g) + 4);
+}
+
+/// What the host's pool plans: a token's keys and values over every layer, a stream's table row fixed.
+pub fn layout(g: Geometry, context: usize) kv.Layout {
+ const row: u64 = 2 * g.kvWidth() * 2;
+ return kv.Layout.ofRows(.{ .kind = .full, .row = row, .layers = @intCast(g.layers) }, page_rows, perSlot(context) * 4, 0);
+}
+
+const dots: Geometry = .{ .hidden = 1536, .layers = 28, .heads = 12, .kv_heads = 2, .head_dim = 128, .vocab = 151936, .eps = 1e-6, .context = 131072, .acc = 2048, .wide = 17920, .mid = 8960, .theta = 1e6 };
+
+test "dots' page: 512 rows of 28 layers' keys and values at 2 x 128 dims, 14 MiB" {
+ try std.testing.expectEqual(@as(u64, 28 * 2 * 512 * 256 * 2), pageBytes(dots));
+ try std.testing.expectEqual(@as(u64, 14680064), pageBytes(dots));
+ const l = layout(dots, 32768);
+ try std.testing.expectEqual(pageBytes(dots), l.pageBytes());
+ try std.testing.expectEqual(@as(u64, 65 * 4), l.fixed_bytes);
+ try std.testing.expectEqual(@as(usize, 65), l.pages(32768 + window));
+ try std.testing.expectEqual(@as(u64, 65 * (14680064 + 4)), streamBytes(dots, 32768));
+}
+
+test "a request holds its prompt, its reply and the verify window in whole pages" {
+ try std.testing.expectEqual(@as(usize, 1), pagesFor(1, 1));
+ try std.testing.expectEqual(@as(usize, 1), pagesFor(400, 96));
+ try std.testing.expectEqual(@as(usize, 2), pagesFor(400, 97));
+ try std.testing.expectEqual(@as(usize, 65), pagesFor(32768, 0));
+ try std.testing.expectEqual(@as(usize, 65), perSlot(32768));
+ try std.testing.expectEqual(@as(usize, 1), perSlot(496));
+ try std.testing.expectEqual(@as(usize, 2), perSlot(497));
+}
diff --git a/zig/src/cuda_dense/pass.zig b/zig/src/cuda_dense/pass.zig
new file mode 100644
index 00000000000..63d9a277094
--- /dev/null
+++ b/zig/src/cuda_dense/pass.zig
@@ -0,0 +1,150 @@
+//! One dense forward: the engine's operators a recipe's layers call, and the pass from token ids to bf16 logits.
+const std = @import("std");
+const cuda = @import("cuda");
+const recipe = @import("recipe.zig");
+const kern = @import("kernels.zig");
+const mdl = @import("model.zig");
+
+const Model = mdl.Model;
+const Kernels = kern.Kernels;
+const Stream = cuda.Stream;
+const Weight = cuda.qmmf.Weight;
+const tail: cuda.norms.Tail = .{ .round = true }; // the norms' torch points: bf16(w * bf16(h * inv))
+
+/// A stream's rows in one forward: its slot, first position, tokens.
+pub const Chain = struct { slot: usize, p0: usize, tokens: []const u32 };
+
+/// What a forward runs on: the model's state, the shared and the recipe's kernels, the weights, the CUDA stream.
+pub fn Engine(comptime R: type) type {
+ return struct { m: *const Model, k: *const Kernels, own: *const R.Kernels, w: *const R.Weights, s: Stream };
+}
+
+/// A forward's rows (a prompt chunk, or a round of windows) and the operators a recipe's layers call on them.
+pub fn Pass(comptime R: type) type {
+ return struct {
+ const P = @This();
+ m: *const Model,
+ k: *const Kernels,
+ own: *const R.Kernels,
+ w: *const R.Weights,
+ s: Stream,
+ g: recipe.Geometry,
+ chains: []const Chain,
+ rows: usize,
+ prompt: bool,
+
+ fn part(p: *const P, w: Weight) !u64 {
+ if (p.k.lane.partBytes(p.rows, w) > p.m.part_bytes) return error.LanePartTooSmall;
+ return p.m.s.part;
+ }
+
+ /// out [rows, n] bf16 = x [rows, k] times `w`, fp32 sums rounded once.
+ pub fn project(p: *const P, w: Weight, x: u64, out: u64) !void {
+ try p.k.lane.matmul(p.s, x, w.k, p.rows, w, out, try p.part(w));
+ }
+
+ /// out [rows, n] fp32 = x [rows, k] times `w`: the sums a recipe's own kernel rounds.
+ pub fn projectF32(p: *const P, w: Weight, x: u64, out: u64) !void {
+ try p.k.lane.matmulF32(p.s, x, w.k, p.rows, w, out, try p.part(w));
+ }
+
+ /// out [rows, n] = bf16(x times `w` + b): the fp32 sums and the bias rounded once (vit.cu's bias).
+ pub fn projectBias(p: *const P, w: Weight, b: u64, x: u64, out: u64) !void {
+ if (w.n > p.m.acc_width) return error.AccTooNarrow;
+ try p.projectF32(w, x, p.m.s.acc);
+ try p.k.vit.bias(p.s, p.m.s.acc, b, out, p.rows, w.n);
+ }
+
+ /// h = bf16(h + r) (r null: as is), x = bf16(w * bf16(h * inv_rms)).
+ pub fn norm(p: *const P, r: ?u64, w: u64) !void {
+ const x = p.m.s;
+ try p.k.norms.addRmsTail(p.s, x.h, r, w, x.h, x.x, null, p.rows, p.g.hidden, p.g.eps, tail);
+ }
+
+ /// out [rows, f] = bf16(bf16(silu(gate)) * up) from gate|up [rows, 2 f].
+ pub fn siluMul(p: *const P, gu: u64, out: u64, f: usize) !void {
+ try p.k.siluMul(p.s, gu, out, p.rows, f);
+ }
+
+ /// Layer `layer`'s keys and values (kv [rows, 2 kv width]) into the pages, then q's attention into out.
+ pub fn attend(p: *const P, layer: usize, q: u64, kv: u64, out: u64) !void {
+ if (layer >= p.g.layers) return error.BadLayer;
+ const m = p.m;
+ const x = m.s;
+ const kc = m.cache(layer, false);
+ const vc = m.cache(layer, true);
+ const qw = p.g.qWidth();
+ var row0: usize = 0;
+ var at: usize = 0;
+ while (at < p.chains.len) : (at += kern.max_segs) {
+ const group = p.chains[at..@min(p.chains.len, at + kern.max_segs)];
+ var sg = std.mem.zeroes(kern.AttnSegs);
+ var rows: usize = 0;
+ for (group, 0..) |ch, j| {
+ sg.kc[j] = kc;
+ sg.vc[j] = vc;
+ sg.meta[j] = x.meta + (at + j) * 4;
+ sg.pt[j] = m.tableOf(ch.slot);
+ sg.row0[j] = @intCast(rows);
+ rows += ch.tokens.len;
+ }
+ sg.ps = @intCast(m.pageElems());
+ sg.n = @intCast(group.len);
+ try p.k.kvWrite(p.s, sg, kv + row0 * 2 * p.g.kvWidth() * 2, rows);
+ const qa = q + row0 * qw * 2;
+ const oa = out + row0 * qw * 2;
+ if (p.prompt) {
+ try p.k.prompt(p.s, qa, kc, vc, oa, group[0].p0, rows, m.tableOf(group[0].slot), m.pageElems());
+ } else {
+ try p.k.attend(p.s, sg, qa, x.po, x.pm, x.pl, oa, x.xs + row0 * (qw / 64) * 4, rows, m.chunks(), m.chunkBlocks());
+ }
+ row0 += rows;
+ }
+ }
+ };
+}
+
+/// Every chain's rows (a prompt chunk, or windows): the recipe's layers, then logits rows [head0, head0 + heads).
+pub fn forward(comptime R: type, e: Engine(R), chains: []const Chain, prompt: bool, head0: usize, heads: usize) !void {
+ const m = e.m;
+ var rows: usize = 0;
+ for (chains) |ch| {
+ if (ch.tokens.len == 0 or ch.slot >= m.slots or ch.p0 + ch.tokens.len > m.caps[ch.slot]) return error.BadChain;
+ rows += ch.tokens.len;
+ }
+ if (rows > @as(usize, if (prompt) mdl.prompt_rows else mdl.round_rows) or (prompt and chains.len != 1)) return error.BadWindow;
+ if (chains.len > mdl.round_rows or heads > mdl.round_rows or head0 + heads > rows) return error.BadWindow;
+ try upload(m, e.s, chains, rows);
+ const p: Pass(R) = .{ .m = m, .k = e.k, .own = e.own, .w = e.w, .s = e.s, .g = m.g, .chains = chains, .rows = rows, .prompt = prompt };
+ try R.embed(&p);
+ for (0..m.g.layers) |i| try R.layer(&p, i);
+ try R.final(&p);
+ if (heads == 0) return;
+ const head: Weight = R.head(e.w);
+ const x = m.s.x + head0 * m.g.hidden * 2;
+ if (e.k.lane.partBytes(heads, head) > m.part_bytes) return error.LanePartTooSmall;
+ try e.k.lane.matmul(e.s, x, m.g.hidden, heads, head, m.s.logits, m.s.part);
+}
+
+fn put(m: *const Model, s: Stream, dst: u64, bytes: []const u8) !void {
+ try mdl.put(m.d, s, dst, bytes);
+}
+
+/// Tokens and positions a row, each chain's first position, staged for the launches.
+fn upload(m: *const Model, s: Stream, chains: []const Chain, rows: usize) !void {
+ const ids = try m.gpa.alloc(i32, 2 * rows + chains.len);
+ defer m.gpa.free(ids);
+ var at: usize = 0;
+ for (chains, 0..) |ch, j| {
+ for (ch.tokens, 0..) |t, i| {
+ ids[at + i] = @intCast(t);
+ ids[rows + at + i] = @intCast(ch.p0 + i);
+ }
+ ids[2 * rows + j] = @intCast(ch.p0);
+ at += ch.tokens.len;
+ }
+ const x = m.s;
+ try put(m, s, x.ids, std.mem.sliceAsBytes(ids[0..rows]));
+ try put(m, s, x.pos, std.mem.sliceAsBytes(ids[rows .. 2 * rows]));
+ try put(m, s, x.meta, std.mem.sliceAsBytes(ids[2 * rows ..]));
+}
diff --git a/zig/src/cuda_dense/put_test.zig b/zig/src/cuda_dense/put_test.zig
new file mode 100644
index 00000000000..236f2303c1d
--- /dev/null
+++ b/zig/src/cuda_dense/put_test.zig
@@ -0,0 +1,72 @@
+//! The engine's uploads own their source: a fake driver reads it at the stream's sync, after the caller changed it.
+const std = @import("std");
+const cuda = @import("cuda");
+const put = @import("model.zig").put;
+const t = std.testing;
+
+const Fake = struct {
+ var pinned: ?[]align(16) u8 = null;
+ var pending: ?[]const u8 = null;
+ var result: [16]u8 = undefined;
+ fn host(out: *?*anyopaque, len: usize, _: c_uint) callconv(.c) cuda.abi.Result {
+ pinned = t.allocator.alignedAlloc(u8, .@"16", len) catch return cuda.abi.error_not_ready;
+ out.* = pinned.?.ptr;
+ return 0;
+ }
+ fn free(_: ?*anyopaque) callconv(.c) cuda.abi.Result {
+ std.debug.assert(pending == null);
+ t.allocator.free(pinned.?);
+ pinned = null;
+ return 0;
+ }
+ fn copy(_: u64, ptr: ?*const anyopaque, len: usize, _: cuda.abi.Stream) callconv(.c) cuda.abi.Result {
+ pending = @as([*]const u8, @ptrCast(ptr.?))[0..len];
+ return 0;
+ }
+ fn sync(_: cuda.abi.Stream) callconv(.c) cuda.abi.Result {
+ if (pending) |bytes| @memcpy(result[0..bytes.len], bytes);
+ pending = null;
+ return 0;
+ }
+ fn event(out: *cuda.abi.Event, _: c_uint) callconv(.c) cuda.abi.Result {
+ out.* = @ptrFromInt(1);
+ return 0;
+ }
+ fn record(_: cuda.abi.Event, _: cuda.abi.Stream) callconv(.c) cuda.abi.Result {
+ return 0;
+ }
+ fn query(_: cuda.abi.Event) callconv(.c) cuda.abi.Result {
+ return if (pending != null) cuda.abi.error_not_ready else 0;
+ }
+ fn destroy(_: cuda.abi.Event) callconv(.c) cuda.abi.Result {
+ return 0;
+ }
+};
+
+test "an upload's source may change as soon as put returns: the device gets the bytes it was given" {
+ var d: cuda.Driver = undefined;
+ d.api.cuMemHostAlloc = Fake.host;
+ d.api.cuMemFreeHost = Fake.free;
+ d.api.cuMemcpyHtoDAsync_v2 = Fake.copy;
+ d.api.cuStreamSynchronize = Fake.sync;
+ d.api.cuEventCreate = Fake.event;
+ d.api.cuEventRecord = Fake.record;
+ d.api.cuEventQuery = Fake.query;
+ d.api.cuEventDestroy_v2 = Fake.destroy;
+ const s: cuda.Stream = .{ .d = &d, .handle = @ptrFromInt(0x1800) };
+ Fake.pending = null;
+ Fake.result = @splat(0);
+ const host_before = cuda.memory.usage(false).host;
+ defer {
+ s.synchronize() catch unreachable;
+ cuda.copy_source.dropSpares(&d, cuda.copy_source.current());
+ }
+ var meta = [4]i32{ 41, 0, 0, 0 };
+ try put(&d, s, 0x1000, std.mem.asBytes(&meta));
+ meta[0] = -7; // a draw's next meta, written into the same stack slot before the stream ran
+ try s.synchronize();
+ cuda.copy_source.dropSpares(&d, cuda.copy_source.current());
+ try t.expectEqual(@as(i32, 41), std.mem.bytesToValue(i32, Fake.result[0..4]));
+ try t.expect(Fake.pinned == null);
+ try t.expectEqual(host_before, cuda.memory.usage(false).host);
+}
diff --git a/zig/src/cuda_dense/recipe.zig b/zig/src/cuda_dense/recipe.zig
new file mode 100644
index 00000000000..b0f35964a52
--- /dev/null
+++ b/zig/src/cuda_dense/recipe.zig
@@ -0,0 +1,136 @@
+//! A dense decoder's recipe contract: its geometry, the decls the engine reads, and the attention launch shape.
+const std = @import("std");
+
+/// A dense GQA decoder's sizes, read from its checkpoint's config before the weights load.
+pub const Geometry = struct {
+ hidden: usize,
+ layers: usize,
+ heads: usize,
+ kv_heads: usize,
+ head_dim: usize, // 64 or 128
+ vocab: usize,
+ eps: f32,
+ context: usize, // the checkpoint's own window
+ acc: usize, // q|k|v width: a fused projection's f32 sums
+ wide: usize, // gate|up width
+ mid: usize, // the MLP's inner width
+ theta: ?f64 = null, // 1-D RoPE base (rope.zig's inverse frequencies on the device); null: no rotary positions
+ inv_freq: ?[]const f32 = null, // the reference's own inverse frequencies, when its pow is not correctly rounded
+
+ pub fn qWidth(g: Geometry) usize {
+ return g.heads * g.head_dim;
+ }
+
+ pub fn kvWidth(g: Geometry) usize {
+ return g.kv_heads * g.head_dim;
+ }
+
+ pub fn group(g: Geometry) usize {
+ return g.heads / g.kv_heads;
+ }
+};
+
+/// Query heads a KV head the decode kernels take at most (paged_attn.cuh's GMAX).
+pub const max_group = 16;
+
+/// The attention launches of one geometry: prompt pattn and its block, decode chunk and merge.
+pub const Shape = struct {
+ pattn: [:0]const u8,
+ chunk: [:0]const u8,
+ merge: [:0]const u8,
+ hpc: usize, // query heads a pattn block takes from one KV head
+ rows: usize, // prompt rows a pattn block takes
+ grid_y: usize, // pattn blocks over the heads
+ shared: u32, // pattn's dynamic shared bytes
+
+ pub fn blocks(s: Shape, rows: usize) usize {
+ return (rows + s.rows - 1) / s.rows;
+ }
+};
+
+fn pattnSymbol(comptime d: usize, comptime hpc: usize) [:0]const u8 {
+ return std.fmt.comptimePrint("_ZN20tf_prefill_attention12pattn_kernelILi{d}ELi8ELi{d}ELi8EEEvPK13__nv_bfloat16S3_S3_PS1_iiiiifPKix", .{ d, hpc });
+}
+
+/// The pattn instances prefill_attention.cu builds at eight warps and eight slots: head dim, query heads a block.
+const Instance = struct { d: usize, hpc: usize, symbol: [:0]const u8 };
+const instances = [_]Instance{
+ .{ .d = 128, .hpc = 8, .symbol = pattnSymbol(128, 8) },
+ .{ .d = 128, .hpc = 2, .symbol = pattnSymbol(128, 2) },
+ .{ .d = 64, .hpc = 4, .symbol = pattnSymbol(64, 4) },
+ .{ .d = 64, .hpc = 2, .symbol = pattnSymbol(64, 2) },
+};
+
+/// pattn's shared bytes: eight staging slots of 32 keys, a key row in 16-byte chunks (prefill_attention_rows.cuh).
+fn pattnShared(d: usize) u32 {
+ const chunks = (2 * ((d + 15) / 16) + 7) / 8 * 8;
+ return @intCast(8 * 32 * chunks * 16);
+}
+
+/// The launches for `g`: the largest built pattn instance whose heads a block divide the group; refusals by name.
+pub fn attentionShape(g: Geometry) error{ UnsupportedDenseHeadDim, UnsupportedDenseGroup }!Shape {
+ if (g.head_dim != 64 and g.head_dim != 128) return error.UnsupportedDenseHeadDim;
+ if (g.kv_heads == 0 or g.heads % g.kv_heads != 0 or g.group() > max_group) return error.UnsupportedDenseGroup;
+ const group_n = g.group();
+ for (instances) |in| if (in.d == g.head_dim and group_n % in.hpc == 0) return .{
+ .pattn = in.symbol,
+ .chunk = if (g.head_dim == 128) "tf_nemo_attn_chunk_segs" else "tf_paged_attn_chunk_segs_64",
+ .merge = if (g.head_dim == 128) "tf_nemo_attn_merge_segs" else "tf_paged_attn_merge_segs_64",
+ .hpc = in.hpc,
+ .rows = 16 * (8 / in.hpc),
+ .grid_y = g.kv_heads * (group_n / in.hpc),
+ .shared = pattnShared(g.head_dim),
+ };
+ return error.UnsupportedDenseGroup;
+}
+
+fn need(comptime R: type, comptime name: []const u8) void {
+ if (!@hasDecl(R, name)) @compileError("a cuda_dense recipe declares " ++ name);
+}
+
+fn absent(comptime R: type, comptime name: []const u8) void {
+ if (@hasDecl(R, name) and @field(R, name) != null) @compileError(name ++ " is not built in the dense engine yet: set it null");
+}
+
+/// The decls a recipe gives the engine, at compile time; the capabilities not built yet must stay null.
+pub fn check(comptime R: type) void {
+ const required = .{ "name", "model_type", "formats", "default_context", "Weights", "Kernels", "geometry", "load", "weightBytes", "embed", "layer", "final", "head" };
+ inline for (required) |name| need(R, name);
+ inline for (.{ "post_embed", "Tower", "scores", "propose" }) |name| absent(R, name);
+}
+
+fn shapeOf(heads: usize, kv_heads: usize, head_dim: usize) !Shape {
+ return attentionShape(.{ .hidden = heads * head_dim, .layers = 1, .heads = heads, .kv_heads = kv_heads, .head_dim = head_dim, .vocab = 1, .eps = 1e-6, .context = 1, .acc = 1, .wide = 1, .mid = 1 });
+}
+
+test "attention shapes: dots 12/2 x 128, 16/8 x 128, 16/8 x 64 and 32/8 x 64" {
+ const dots = try shapeOf(12, 2, 128);
+ try std.testing.expectEqual(@as(usize, 2), dots.hpc);
+ try std.testing.expectEqual(@as(usize, 64), dots.rows);
+ try std.testing.expectEqual(@as(usize, 6), dots.grid_y);
+ try std.testing.expectEqual(@as(u32, 65536), dots.shared);
+ try std.testing.expectEqualStrings("_ZN20tf_prefill_attention12pattn_kernelILi128ELi8ELi2ELi8EEEvPK13__nv_bfloat16S3_S3_PS1_iiiiifPKix", dots.pattn);
+ try std.testing.expectEqualStrings("tf_nemo_attn_chunk_segs", dots.chunk);
+ try std.testing.expectEqual(@as(usize, 2), dots.blocks(65));
+ const glm = try shapeOf(16, 8, 128);
+ try std.testing.expect(glm.hpc == 2 and glm.rows == 64 and glm.grid_y == 8 and glm.shared == 65536);
+ const parse = try shapeOf(16, 8, 64);
+ try std.testing.expect(parse.hpc == 2 and parse.rows == 64 and parse.grid_y == 8 and parse.shared == 32768);
+ try std.testing.expectEqualStrings("_ZN20tf_prefill_attention12pattn_kernelILi64ELi8ELi2ELi8EEEvPK13__nv_bfloat16S3_S3_PS1_iiiiifPKix", parse.pattn);
+ try std.testing.expectEqualStrings("tf_paged_attn_merge_segs_64", parse.merge);
+ const lfm = try shapeOf(32, 8, 64);
+ try std.testing.expect(lfm.hpc == 4 and lfm.rows == 32 and lfm.grid_y == 8 and lfm.shared == 32768);
+ try std.testing.expectEqualStrings("_ZN20tf_prefill_attention12pattn_kernelILi64ELi8ELi4ELi8EEEvPK13__nv_bfloat16S3_S3_PS1_iiiiifPKix", lfm.pattn);
+}
+
+test "attention shapes refuse groups past 16, odd groups, uneven heads and head dims other than 64 and 128" {
+ try std.testing.expectError(error.UnsupportedDenseGroup, shapeOf(34, 2, 128));
+ try std.testing.expectError(error.UnsupportedDenseGroup, shapeOf(12, 4, 128));
+ try std.testing.expectError(error.UnsupportedDenseGroup, shapeOf(12, 5, 64));
+ try std.testing.expectError(error.UnsupportedDenseGroup, shapeOf(8, 8, 64));
+ try std.testing.expectError(error.UnsupportedDenseHeadDim, shapeOf(16, 8, 80));
+ try std.testing.expectError(error.UnsupportedDenseHeadDim, shapeOf(16, 8, 256));
+ try std.testing.expectEqual(@as(usize, 2), (try shapeOf(32, 8, 128)).hpc); // group 4 at head 128: no 4-head block
+ try std.testing.expectEqual(@as(usize, 8), (try shapeOf(32, 2, 128)).hpc);
+ try std.testing.expectEqual(@as(usize, 8), (try shapeOf(64, 8, 128)).hpc);
+}
diff --git a/zig/src/cuda_dense/root.zig b/zig/src/cuda_dense/root.zig
new file mode 100644
index 00000000000..c20f45e0e5e
--- /dev/null
+++ b/zig/src/cuda_dense/root.zig
@@ -0,0 +1,30 @@
+//! The shared CUDA dense-decoder engine: a recipe's layers over paged GQA attention, lanes, draws and the host decls.
+pub const recipe = @import("recipe.zig");
+pub const pages = @import("pages.zig");
+pub const rope = @import("rope.zig");
+pub const kernels = @import("kernels.zig");
+pub const model = @import("model.zig");
+pub const pass = @import("pass.zig");
+pub const draws = @import("draws.zig");
+pub const backend = @import("backend.zig");
+pub const native = @import("native.zig");
+
+pub const Native = native.Native;
+pub const Pass = pass.Pass;
+pub const Geometry = recipe.Geometry;
+pub const check = recipe.check;
+
+test {
+ _ = recipe;
+ _ = pages;
+ _ = rope;
+ _ = kernels;
+ _ = model;
+ _ = pass;
+ _ = draws;
+ _ = backend;
+ _ = native;
+ _ = @import("native_test.zig");
+ _ = @import("put_test.zig");
+ _ = @import("device_fail_test.zig");
+}
diff --git a/zig/src/cuda_dense/rope.zig b/zig/src/cuda_dense/rope.zig
new file mode 100644
index 00000000000..e49633f0ef4
--- /dev/null
+++ b/zig/src/cuda_dense/rope.zig
@@ -0,0 +1,31 @@
+//! 1-D rotary inverse frequencies as HF's default rope computes them in float32; angles and cos/sin are device work.
+const std = @import("std");
+
+/// Frequencies a head of `dim` dims takes: one a rotated pair.
+pub fn count(dim: usize) usize {
+ return dim / 2;
+}
+
+/// out[i] = 1 / theta^(2i / dim): the exponent an f32 quotient, the power of the f32 operands rounded once to f32.
+pub fn inverse(theta: f64, dim: usize, out: []f32) void {
+ std.debug.assert(out.len == count(dim));
+ const base: f32 = @floatCast(theta);
+ for (out, 0..) |*v, i| {
+ const e: f32 = @as(f32, @floatFromInt(2 * i)) / @as(f32, @floatFromInt(dim));
+ const pw: f32 = @floatCast(std.math.pow(f64, base, e));
+ v.* = @as(f32, 1.0) / pw;
+ }
+}
+
+test "dots' inverse frequencies (theta 1e6, head dim 128) in float32" {
+ var got: [64]f32 = undefined;
+ inverse(1e6, 128, &got);
+ for (got, 0..) |v, i| {
+ const e = @as(f32, @floatFromInt(i * 2)) / 128.0;
+ const want = @as(f32, 1.0) / @as(f32, @floatCast(std.math.pow(f64, @as(f32, 1e6), e)));
+ try std.testing.expectEqual(@as(u32, @bitCast(want)), @as(u32, @bitCast(v)));
+ }
+ try std.testing.expectEqual(@as(f32, 1), got[0]);
+ try std.testing.expectEqual(@as(f32, 1e-3), got[32]); // theta^(1/2): 1000 exactly, its f32 reciprocal
+ try std.testing.expect(got[63] < got[62] and got[63] > 1e-6);
+}
diff --git a/zig/src/families/dots_mocr/cuda.zig b/zig/src/families/dots_mocr/cuda.zig
new file mode 100644
index 00000000000..837f383434b
--- /dev/null
+++ b/zig/src/families/dots_mocr/cuda.zig
@@ -0,0 +1,61 @@
+//! dots.mocr's text decoder on CUDA: the dots recipe behind the shared dense engine (native/cuda.zig's family decls).
+const std = @import("std");
+const dense = @import("cuda_dense");
+const cfg = @import("config.zig");
+const fixtures = @import("config_test.zig");
+
+pub const recipe = @import("cuda_recipe.zig");
+pub const weights = @import("cuda_weights.zig");
+pub const kernels = @import("cuda_kernels.zig");
+pub const native = dense.Native(recipe);
+
+test "dots geometry from the released config: 12 query heads over 2 KV heads of 128, two query heads a pattn block" {
+ const c = try cfg.Config.parse(std.testing.allocator, fixtures.sample, fixtures.generation);
+ const g = recipe.geometryOf(c);
+ var plain = g;
+ plain.inv_freq = null;
+ try std.testing.expectEqual(dense.Geometry{ .hidden = 1536, .layers = 28, .heads = 12, .kv_heads = 2, .head_dim = 128, .vocab = 151936, .eps = 1e-6, .context = 131072, .acc = 2048, .wide = 17920, .mid = 8960, .theta = 1e6 }, plain);
+ try std.testing.expectEqual(@as(usize, 64), g.inv_freq.?.len); // Transformers' own table (cuda_recipe.zig)
+ const shape = try dense.recipe.attentionShape(g);
+ try std.testing.expect(shape.hpc == 2 and shape.grid_y == 6 and shape.rows == 64);
+}
+
+test "dots text bytes: embedding, untied head, final norm and 28 layers, the tower left out" {
+ const c = try cfg.Config.parse(std.testing.allocator, fixtures.sample, fixtures.generation);
+ const h: u64 = 1536;
+ const kv: u64 = 256;
+ const m: u64 = 8960;
+ const layer = 2 * h + (h + 2 * kv) * h + (h + 2 * kv) + h * h + 3 * m * h;
+ const want = (2 * 151936 * h + h + 28 * layer) * 2;
+ try std.testing.expectEqual(@as(u64, 3554176000), want);
+ try std.testing.expectEqual(want, try weights.textBytes(std.testing.allocator, c));
+}
+
+test "dots on CUDA: text only, no reasoning block, its config and tensor refusals worded" {
+ try std.testing.expect(!native.reasoning and native.keeps_no_prompts);
+ // compiles the load and the layer
+ const hooks = .{ &native.open, &native.attach, &native.fits, &native.ledgerPool, &native.textWeightBytes };
+ inline for (hooks) |h| try std.testing.expect(@intFromPtr(h) != 0);
+ try std.testing.expectEqualStrings("dots_ocr", native.model_type);
+ try std.testing.expectStringStartsWith(native.explain(null, error.DenseTextOnly).?, "dots.mocr on CUDA reads text prompts only");
+ for ([_]anyerror{ error.UnsupportedDotsConfig, error.BadDotsConfig, error.BadEos, error.DotsShape, error.DotsMissingTensor, error.DotsTensorCount, error.DotsIndexMismatch }) |e|
+ try std.testing.expect(native.explain(null, e) != null);
+}
+
+test "dots on CUDA reads its geometry from the checkpoint's files and refuses another model's config by name" {
+ const a = std.testing.allocator;
+ const io = std.testing.io;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(io, .{ .sub_path = "generation_config.json", .data = fixtures.generation });
+ try tmp.dir.writeFile(io, .{ .sub_path = "config.json", .data = fixtures.sample });
+ const dir = try std.fmt.allocPrint(a, ".zig-cache/tmp/{s}", .{tmp.sub_path});
+ defer a.free(dir);
+ try std.testing.expectEqual(@as(usize, 2048), (try recipe.geometry(a, io, dir)).acc);
+ try std.testing.expectError(error.FileNotFound, recipe.weightBytes(a, io, dir)); // no shards: nothing to count
+ const other = try std.mem.replaceOwned(u8, a, fixtures.sample, "\"dots_ocr\"", "\"qwen3_5\"");
+ defer a.free(other);
+ try tmp.dir.writeFile(io, .{ .sub_path = "config.json", .data = other });
+ try std.testing.expectError(error.UnsupportedDotsConfig, recipe.geometry(a, io, dir));
+ try std.testing.expect(native.explain(null, error.UnsupportedDotsConfig) != null);
+}
diff --git a/zig/src/families/dots_mocr/cuda_kernels.zig b/zig/src/families/dots_mocr/cuda_kernels.zig
new file mode 100644
index 00000000000..39a101a2e0c
--- /dev/null
+++ b/zig/src/families/dots_mocr/cuda_kernels.zig
@@ -0,0 +1,31 @@
+//! dots.mocr's own CUDA launch beside the dense engine's: the q|k|v bias and Torch-order RoPE (dots_mocr.cu).
+const cuda = @import("cuda");
+
+/// Threads a (row, head) block takes: dims j and j + 64 of a 128-dim head.
+const threads = 64;
+
+pub const Kernels = struct {
+ mod: cuda.Module,
+ rope: cuda.Function,
+
+ pub fn load(ctx: *const cuda.Context) !Kernels {
+ if (!cuda.kernels.available) return error.BuiltWithoutKernels;
+ var mod = try cuda.Module.load(ctx.d, cuda.kernels.dots_mocr);
+ errdefer mod.unload();
+ return .{ .mod = mod, .rope = try mod.function("tf_dots_qkv_rope") };
+ }
+
+ pub fn deinit(k: *Kernels) void {
+ k.mod.unload();
+ }
+
+ /// q [rows, h, 128] and kv [rows, k hk | v hk] from fp32 acc [rows, (h + 2 hk) 128] plus bias, rotated at pos.
+ pub fn qkvRope(k: *const Kernels, s: cuda.Stream, acc: u64, bias: u64, inv: u64, pos: u64, q: u64, kv: u64, rows: usize, h: usize, hk: usize) !void {
+ if (rows == 0) return;
+ var a: cuda.Args = .{};
+ for ([_]u64{ acc, bias, inv, pos, q, kv }) |v| a.add(v);
+ a.add(@as(c_int, @intCast(h)));
+ a.add(@as(c_int, @intCast(hk)));
+ try cuda.launch.launch(k.rope, .{ .grid = .{ .x = @intCast(rows), .y = @intCast(h + 2 * hk), .z = 1 }, .block = .{ .x = threads } }, s, &a);
+ }
+};
diff --git a/zig/src/families/dots_mocr/cuda_recipe.zig b/zig/src/families/dots_mocr/cuda_recipe.zig
new file mode 100644
index 00000000000..3052c4d69a1
--- /dev/null
+++ b/zig/src/families/dots_mocr/cuda_recipe.zig
@@ -0,0 +1,97 @@
+//! dots.mocr's Qwen2 text decoder as a cuda_dense recipe: HIP's rounding points, its tower refused until it is built.
+const std = @import("std");
+const cuda = @import("cuda");
+const dense = @import("cuda_dense");
+const cfg = @import("config.zig");
+const wts = @import("cuda_weights.zig");
+const kern = @import("cuda_kernels.zig");
+
+const Recipe = @This();
+const P = *const dense.Pass(Recipe);
+
+pub const name = "dots.mocr";
+pub const model_type = "dots_ocr";
+pub const formats: []const []const u8 = &.{"bf16"};
+pub const default_context: i64 = 32768;
+/// Replies carry no reasoning block: the text is content.
+pub const reasoning = false;
+
+pub const Weights = wts.Weights;
+pub const Kernels = kern.Kernels;
+
+/// The engine's geometry of an admitted dots.mocr config.
+pub fn geometryOf(c: cfg.Config) dense.Geometry {
+ const h: usize = c.hidden;
+ return .{ .hidden = h, .layers = c.layers, .heads = c.heads, .kv_heads = c.kv_heads, .head_dim = c.headDim(), .vocab = c.vocab, .eps = c.eps, .context = c.context, .acc = h + 2 * @as(usize, c.kvWidth()), .wide = 2 * @as(usize, c.intermediate), .mid = c.intermediate, .theta = c.theta, .inv_freq = if (c.theta == 1e6 and c.headDim() == 128) &hf_inv_freq else null };
+}
+
+/// config.json and generation_config.json admitted as dots.mocr's, before any tensor is read.
+pub fn geometry(a: std.mem.Allocator, io: std.Io, dir: []const u8) !dense.Geometry {
+ return geometryOf(try cfg.Config.read(a, io, dir));
+}
+
+pub fn load(gpa: std.mem.Allocator, io: std.Io, d: *const cuda.Driver, s: cuda.Stream, dir: []const u8) !Weights {
+ return wts.load(gpa, io, d, s, dir);
+}
+
+/// The text decoder's bytes on the device, once every shard's header is checked.
+pub fn weightBytes(a: std.mem.Allocator, io: std.Io, dir: []const u8) !u64 {
+ const c = try cfg.Config.read(a, io, dir);
+ _ = try @import("weights.zig").inspect(a, io, dir, c);
+ return wts.textBytes(a, c);
+}
+
+pub fn embed(p: P) !void {
+ try p.k.embed.bf16(p.s, p.m.s.ids, p.w.embed, p.m.s.h, p.rows, p.g.hidden);
+}
+
+/// One Qwen2 layer: RMS, q|k|v sums, bias and RoPE in Torch's order, attention, o, RMS, SwiGLU.
+pub fn layer(p: P, i: usize) !void {
+ const x = p.m.s;
+ const l = p.w.layers[i];
+ try p.norm(if (i == 0) null else x.r, l.ln1);
+ try p.projectF32(l.qkv, x.x, x.acc);
+ try p.own.qkvRope(p.s, x.acc, l.bias, p.m.inv, x.pos, x.q, x.kv, p.rows, p.g.heads, p.g.kv_heads);
+ try p.attend(i, x.q, x.kv, x.y);
+ try p.project(l.o, x.y, x.r);
+ try p.norm(x.r, l.ln2);
+ try p.project(l.gate_up, x.x, x.wide);
+ try p.siluMul(x.wide, x.y, p.g.mid);
+ try p.project(l.down, x.y, x.r);
+}
+
+pub fn final(p: P) !void {
+ try p.norm(p.m.s.r, p.w.norm);
+}
+
+pub fn head(w: *const Weights) cuda.qmmf.Weight {
+ return w.head;
+}
+
+pub fn explain(err: anyerror) ?[]const u8 {
+ return switch (err) {
+ error.UnsupportedDotsConfig, error.BadDotsConfig, error.BadEos => "this checkpoint's config is not dots.mocr's (DotsOCRForCausalLM, Qwen2 1.5B text decoder in BF16), the one this engine serves",
+ error.DotsDType, error.DotsShape, error.DotsExtent, error.DotsMissingTensor, error.DotsTensorCount, error.DotsIndexMismatch => "this dots.mocr checkpoint's tensors are not the released BF16 ones: download it again",
+ else => null,
+ };
+}
+
+/// Transformers' own float32 inv_freq for theta 1e6 at head dim 128 (fixtures/README.md): Torch's CPU pow, bit for bit.
+const hf_inv_freq: [64]f32 = blk: {
+ @setEvalBranchQuota(20_000);
+ const text = @embedFile("fixtures/rope_inv_freq.txt");
+ var out: [64]f32 = undefined;
+ var it = std.mem.tokenizeAny(u8, text, ", \n");
+ for (&out) |*v| v.* = @bitCast(std.fmt.parseInt(u32, it.next().?, 0) catch unreachable);
+ if (it.next() != null) @compileError("fixtures/rope_inv_freq.txt holds more than 64 values");
+ break :blk out;
+};
+
+test "dots' rotary frequencies are Transformers' own: the correctly rounded pow differs at entry 37 only" {
+ var mine: [64]f32 = undefined;
+ @import("cuda_dense").rope.inverse(1e6, 128, &mine);
+ for (hf_inv_freq, mine, 0..) |hf, own, i| {
+ const one_ulp = @as(u32, @bitCast(hf)) -% @as(u32, @bitCast(own)) <= 1 or @as(u32, @bitCast(own)) -% @as(u32, @bitCast(hf)) <= 1;
+ try std.testing.expect(if (i == 37) hf != own and one_ulp else hf == own);
+ }
+}
diff --git a/zig/src/families/dots_mocr/cuda_weights.zig b/zig/src/families/dots_mocr/cuda_weights.zig
new file mode 100644
index 00000000000..edda9806954
--- /dev/null
+++ b/zig/src/families/dots_mocr/cuda_weights.zig
@@ -0,0 +1,231 @@
+//! dots.mocr's text decoder on CUDA: BF16 tensors on the device, q|k|v and gate|up fused into packed lane weights.
+const std = @import("std");
+const cuda = @import("cuda");
+const core = @import("core");
+const cfg = @import("config.zig");
+const inventory = @import("weights.zig");
+
+const DeviceBuffer = cuda.DeviceBuffer;
+const Weight = cuda.qmmf.Weight;
+
+/// The text decoder's names: the tower's (vision_tower.) stay on the host until the tower is built on CUDA.
+const text = [_][]const u8{ "model.", "lm_head." };
+
+pub const Layer = struct {
+ ln1: u64, // bf16 [hidden]: input_layernorm
+ ln2: u64, // bf16 [hidden]: post_attention_layernorm
+ qkv: Weight, // q|k|v [hidden + 2 kv width, hidden]
+ bias: u64, // bf16 [hidden + 2 kv width]: q|k|v biases
+ o: Weight,
+ gate_up: Weight, // gate|up [2 intermediate, hidden]
+ down: Weight,
+};
+
+pub const Weights = struct {
+ gpa: std.mem.Allocator,
+ config: cfg.Config,
+ buffers: std.ArrayList(DeviceBuffer) = .empty,
+ layers: []Layer = &.{},
+ embed: u64 = 0, // bf16 [vocab, hidden]
+ norm: u64 = 0, // bf16 [hidden]: model.norm
+ head: Weight = undefined, // the untied lm_head
+
+ pub fn deinit(w: *Weights) void {
+ for (w.buffers.items) |*b| b.free();
+ w.buffers.deinit(w.gpa);
+ w.gpa.free(w.layers);
+ }
+
+ fn alloc(w: *Weights, d: *const cuda.Driver, bytes: usize) !DeviceBuffer {
+ try w.buffers.ensureUnusedCapacity(w.gpa, 1);
+ const b = try DeviceBuffer.alloc(d, bytes);
+ w.buffers.appendAssumeCapacity(b);
+ return b;
+ }
+};
+
+/// The text tensors' bytes as stored, which their device copies take too (every lane weight's rows a multiple of 128).
+pub fn textBytes(gpa: std.mem.Allocator, c: cfg.Config) !u64 {
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ var total: u64 = 0;
+ for (try inventory.inventory(arena.allocator(), c)) |s| {
+ for (text) |p| if (std.mem.startsWith(u8, s.name, p)) {
+ total += s.bytes();
+ };
+ }
+ return total;
+}
+
+/// Reads the checkpoint's text tensors into the device on `s`, fusing and packing the projections.
+const Loader = struct {
+ w: *Weights,
+ ck: *core.Checkpoint,
+ d: *const cuda.Driver,
+ s: cuda.Stream,
+ pack: *const cuda.modelopt.Packer,
+ stage: DeviceBuffer,
+
+ /// A bf16 tensor as stored.
+ fn raw(l: *Loader, name: []const u8, shape: []const usize) !u64 {
+ return l.joined(&.{name}, &.{shape[0]}, if (shape.len == 2) shape[1] else 1);
+ }
+
+ /// bf16 tensors [rows[i], k] one after another in a buffer of their own.
+ fn joined(l: *Loader, names: []const []const u8, rows: []const usize, k: usize) !u64 {
+ var n: usize = 0;
+ for (rows) |r| n += r;
+ const b = try l.w.alloc(l.d, n * k * 2);
+ var at: usize = 0;
+ for (names, rows) |name, r| {
+ const t = try l.ck.expect(name, .bf16, if (k == 1) &.{r} else &.{ r, k });
+ try b.upload(at, t.bytes);
+ at += t.bytes.len;
+ }
+ return b.ptr;
+ }
+
+ /// bf16 projections [rows[i], k] stacked into one [sum rows, k] lane weight, packed on the device.
+ fn lane(l: *Loader, names: []const []const u8, rows: []const usize, k: usize) !Weight {
+ var n: usize = 0;
+ var at: usize = 0;
+ for (names, rows) |name, r| {
+ const t = try l.ck.expect(name, .bf16, &.{ r, k });
+ try l.stage.upload(at, t.bytes);
+ at += t.bytes.len;
+ n += r;
+ }
+ const b = try l.w.alloc(l.d, cuda.modelopt.padded(n) * k * 2);
+ try l.pack.bf16Lane(l.s, l.stage.ptr, b.ptr, n, k);
+ try l.s.synchronize(); // the next projection's upload reuses the stage
+ return cuda.modelopt.bf16Weight(b.ptr, n, k);
+ }
+
+ /// A failed load's work proved complete (the packing stream, then the uploads' null stream) before buffers go.
+ fn settle(l: *const Loader) void {
+ cuda.completion.drain(l.d, l.s, "a dots.mocr weight load did not drain before its buffers were freed");
+ cuda.completion.drain(l.d, .{ .d = l.d, .handle = null }, "dots.mocr weight uploads did not drain before their buffers were freed");
+ }
+};
+
+/// The text decoder of the dots.mocr checkpoint at `dir`, every header checked first; a tensor left over refuses.
+pub fn load(gpa: std.mem.Allocator, io: std.Io, d: *const cuda.Driver, s: cuda.Stream, dir: []const u8) !Weights {
+ const c = try cfg.Config.read(gpa, io, dir);
+ _ = try inventory.inspect(gpa, io, dir, c);
+ var ck = try core.Checkpoint.openModelPrefixes(gpa, io, dir, &text);
+ defer ck.close();
+ var pack = try cuda.modelopt.Packer.load(d);
+ defer pack.unload();
+ var w: Weights = .{ .gpa = gpa, .config = c };
+ errdefer w.deinit();
+ w.layers = try gpa.alloc(Layer, c.layers);
+ const h: usize = c.hidden;
+ const kvw: usize = c.kvWidth();
+ const m: usize = c.intermediate;
+ var stage = try DeviceBuffer.alloc(d, @as(usize, c.vocab) * h * 2);
+ defer stage.free();
+ var l: Loader = .{ .w = &w, .ck = &ck, .d = d, .s = s, .pack = &pack, .stage = stage };
+ errdefer l.settle(); // runs first on failure: the stage, the weights and the packer go after it
+ w.embed = try l.raw("model.embed_tokens.weight", &.{ c.vocab, h });
+ w.norm = try l.raw("model.norm.weight", &.{h});
+ w.head = try l.lane(&.{"lm_head.weight"}, &.{c.vocab}, h);
+ var names: [7][96]u8 = undefined;
+ for (w.layers, 0..) |*y, i| {
+ var n: [7][]const u8 = undefined;
+ for (&n, &names, [_][]const u8{ "self_attn.q_proj", "self_attn.k_proj", "self_attn.v_proj", "self_attn.o_proj", "mlp.gate_proj", "mlp.up_proj", "mlp.down_proj" }) |*out, *buf, part|
+ out.* = try std.fmt.bufPrint(buf, "model.layers.{d}.{s}.weight", .{ i, part });
+ var bias: [3][96]u8 = undefined;
+ var b: [3][]const u8 = undefined;
+ for (&b, &bias, n[0..3]) |*out, *buf, weight| out.* = try std.fmt.bufPrint(buf, "{s}.bias", .{weight[0 .. weight.len - ".weight".len]});
+ var norms: [2][96]u8 = undefined;
+ y.* = .{
+ .ln1 = try l.raw(try std.fmt.bufPrint(&norms[0], "model.layers.{d}.input_layernorm.weight", .{i}), &.{h}),
+ .ln2 = try l.raw(try std.fmt.bufPrint(&norms[1], "model.layers.{d}.post_attention_layernorm.weight", .{i}), &.{h}),
+ .qkv = try l.lane(n[0..3], &.{ h, kvw, kvw }, h),
+ .bias = try l.joined(&b, &.{ h, kvw, kvw }, 1),
+ .o = try l.lane(n[3..4], &.{h}, h),
+ .gate_up = try l.lane(n[4..6], &.{ m, m }, h),
+ .down = try l.lane(n[6..7], &.{h}, m),
+ };
+ }
+ if (ck.unused() != 0) return error.DotsTensorCount;
+ try s.synchronize();
+ return w;
+}
+
+/// A device whose packing stream's wait fails: it counts buffers freed while a launch may still write them.
+const Fake = struct {
+ const queue: cuda.abi.Stream = @ptrFromInt(0x1800);
+ var next: u64 = 0x10000;
+ var live: usize = 0;
+ var pending = false;
+ var freed_pending: usize = 0;
+ var proofs: usize = 0;
+ fn alloc(out: *u64, len: usize) callconv(.c) cuda.abi.Result {
+ out.* = next;
+ next += len + 256;
+ live += 1;
+ return 0;
+ }
+ fn free(_: u64) callconv(.c) cuda.abi.Result {
+ freed_pending += @intFromBool(pending);
+ live -= 1;
+ return 0;
+ }
+ fn copy(_: u64, _: ?*const anyopaque, _: usize) callconv(.c) cuda.abi.Result {
+ return 0;
+ }
+ fn launch(_: cuda.abi.Function, _: c_uint, _: c_uint, _: c_uint, _: c_uint, _: c_uint, _: c_uint, _: c_uint, _: cuda.abi.Stream, _: ?[*]?*anyopaque, _: ?[*]?*anyopaque) callconv(.c) cuda.abi.Result {
+ pending = true;
+ return 0;
+ }
+ fn wait(s: cuda.abi.Stream) callconv(.c) cuda.abi.Result {
+ return if (s == queue) cuda.abi.error_not_ready else 0;
+ }
+ fn context() callconv(.c) cuda.abi.Result {
+ proofs += 1;
+ pending = false;
+ return 0;
+ }
+ fn driver() cuda.Driver {
+ var d: cuda.Driver = .{ .lib = undefined, .api = undefined };
+ d.api.cuMemAlloc_v2 = alloc;
+ d.api.cuMemFree_v2 = free;
+ d.api.cuMemcpyHtoD_v2 = copy;
+ d.api.cuLaunchKernel = launch;
+ d.api.cuStreamSynchronize = wait;
+ d.api.cuCtxSynchronize = context;
+ return d;
+ }
+};
+
+test "a failed pack wait proves completion by the context before the stage or any weight is freed" {
+ const a = std.testing.allocator;
+ const io = std.testing.io;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const header = "{\"x.weight\":{\"dtype\":\"BF16\",\"shape\":[2,64],\"data_offsets\":[0,256]}}";
+ var file: [8 + header.len + 256]u8 = @splat(0);
+ std.mem.writeInt(u64, file[0..8], header.len, .little);
+ @memcpy(file[8..][0..header.len], header);
+ try tmp.dir.writeFile(io, .{ .sub_path = "model.safetensors", .data = &file });
+ const dir = try std.fmt.allocPrint(a, ".zig-cache/tmp/{s}", .{tmp.sub_path});
+ defer a.free(dir);
+ var ck = try core.Checkpoint.openModelPrefixes(a, io, dir, &.{});
+ defer ck.close();
+ var d = Fake.driver();
+ var pack: cuda.modelopt.Packer = undefined;
+ pack.bf16_lane = .{ .d = &d, .handle = @ptrFromInt(1) };
+ var w: Weights = .{ .gpa = a, .config = undefined };
+ var stage = try DeviceBuffer.alloc(&d, 256);
+ const l: Loader = .{ .w = &w, .ck = &ck, .d = &d, .s = .{ .d = &d, .handle = Fake.queue }, .pack = &pack, .stage = stage };
+ var run = l;
+ try std.testing.expectError(error.NotReady, run.lane(&.{"x.weight"}, &.{2}, 64));
+ try std.testing.expect(Fake.pending and Fake.live == 2);
+ l.settle();
+ stage.free();
+ w.deinit();
+ try std.testing.expectEqual(@as(usize, 0), Fake.freed_pending);
+ try std.testing.expectEqual(@as(usize, 0), Fake.live);
+ try std.testing.expectEqual(@as(usize, 1), Fake.proofs);
+}
diff --git a/zig/src/families/dots_mocr/fixtures/README.md b/zig/src/families/dots_mocr/fixtures/README.md
index f75f365ad29..75fbffe9e66 100644
--- a/zig/src/families/dots_mocr/fixtures/README.md
+++ b/zig/src/families/dots_mocr/fixtures/README.md
@@ -50,3 +50,9 @@ FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE \
OUTPUT_DIR --pci PCI_ID --max-new-tokens 64
python tools/zig/dots_mocr_reference.py compare OUTPUT_DIR NATIVE_PREFIX
```
+
+`rope_inv_freq.txt` holds the 64 float32 bit patterns of the checkpoint's
+rotary `inv_freq` as Transformers 4.57.6 computes them on the CPU (theta 1e6,
+head dim 128), dumped by the oracle's `text` mode (`rope-inv_freq.npy`). The
+CUDA recipe uploads them as they are, because Torch's CPU `powf` is not
+correctly rounded: entry 37 is one ulp from the correctly rounded value.
diff --git a/zig/src/families/dots_mocr/fixtures/rope_inv_freq.txt b/zig/src/families/dots_mocr/fixtures/rope_inv_freq.txt
new file mode 100644
index 00000000000..8d31a9bc1db
--- /dev/null
+++ b/zig/src/families/dots_mocr/fixtures/rope_inv_freq.txt
@@ -0,0 +1 @@
+0x3f800000,0x3f4e4bad,0x3f263de0,0x3f05f6ef,0x3ed7e89b,0x3eadfcff,0x3e8c3504,0x3e61f835,0x3e361887,0x3e12bd91,0x3dec7fd6,0x3dbe94c6,0x3d99940d,0x3d778513,0x3d47763f,0x3d20bc1d,0x3d0186e3,0x3cd0c1a8,0x3ca8398b,0x3c879008,0x3c5a7bf2,0x3c301052,0x3c0de12d,0x3be4aa46,0x3bb8449c,0x3b947dae,0x3b6f520e,0x3b40dac5,0x3b1b690d,0x3afa78f0,0x3ac9d75c,0x3aa2a6f7,0x3a83126f,0x3a533f28,0x3a2a3b44,0x3a092e02,0x39dd1725,0x39b229fb,0x398f9272,0x39676492,0x393a7753,0x39164324,0x38f22ce2,0x38c327b5,0x389d43a4,0x387d75d4,0x384c3fbe,0x382497ab,0x3804a2b2,0x37d5c441,0x37ac431d,0x378ad0ed,0x375fba4f,0x37344a0e,0x371148e3,0x36ea2732,0x36bcb0c1,0x36980e02,0x36751070,0x36457bab,0x361f23e4,0x36003dec,0x35ceaf79,0x35a68e4c
diff --git a/zig/src/families/glm/cuda_experts.zig b/zig/src/families/glm/cuda_experts.zig
index 8eca3815e3b..48f4c2b5c71 100644
--- a/zig/src/families/glm/cuda_experts.zig
+++ b/zig/src/families/glm/cuda_experts.zig
@@ -60,13 +60,13 @@ pub fn run(ks: *const Kernels, stream: cuda.Stream, which: Which, l: cuda.expert
};
const in: cuda.experts.Rows = .{ .x = x, .stride = x_stride, .slots = plan.slots };
const pairs = rows * plan.slots;
- if (plan.prompt) return g.prompt(stream, epi, in, l, plan.route, out, maxItems(pairs, plan.experts, staged_tile), skip);
+ if (plan.prompt) return g.prompt(stream, epi, in, l, plan.route, out, maxItems(pairs, plan.experts, staged_tile), pairs, staged_tile, skip);
if (plan.fact) {
// gate-up reads two matrices a K tile and gains from the factored tiles; down is short and keeps the waves
if (epi == .up and !l.relu2) return cuda.experts.Nvfp4Experts.runFact(&ks.nvfp4, stream, epi, in, l, plan.route, out, maxItems(pairs, plan.experts, staged_tile), skip);
return cuda.experts_waves.run(&ks.nvfp4, stream, epi, in, l, plan.route, out, maxItems(pairs, plan.experts, tile), skip);
}
- return g.decode(stream, epi, in, l, plan.route, out, maxItems(pairs, plan.experts, tile), skip);
+ return g.decode(stream, epi, in, l, plan.route, out, maxItems(pairs, plan.experts, tile), pairs, tile, skip);
}
test "block 32's kernels are wanted for a csf0_v1 module alone, not for block 16's recipes or a single-format NVFP4" {
diff --git a/zig/src/families/kolibri1/forward.zig b/zig/src/families/kolibri1/forward.zig
index dcfa7d3abb8..28b504135cc 100644
--- a/zig/src/families/kolibri1/forward.zig
+++ b/zig/src/families/kolibri1/forward.zig
@@ -464,11 +464,11 @@ pub const Model = struct {
const up_in: experts.Rows = .{ .x = m.s.x, .stride = c.hidden, .slots = slots_e };
const down_in: experts.Rows = .{ .x = m.s.act, .stride = c.moe_width };
if (prompt) {
- try gx.prompt(s, .up, up_in, L.experts, m.s.plan, m.s.act, items, -1);
- try gx.prompt(s, .down_bf16, down_in, L.experts, m.s.plan, m.s.y, items, -1);
+ try gx.prompt(s, .up, up_in, L.experts, m.s.plan, m.s.act, items, pairs, tile, -1);
+ try gx.prompt(s, .down_bf16, down_in, L.experts, m.s.plan, m.s.y, items, pairs, tile, -1);
} else {
- try gx.decode(s, .up, up_in, L.experts, m.s.plan, m.s.act, items, -1);
- try gx.decode(s, .down_f32, down_in, L.experts, m.s.plan, m.s.y, items, -1);
+ try gx.decode(s, .up, up_in, L.experts, m.s.plan, m.s.act, items, pairs, tile, -1);
+ try gx.decode(s, .down_f32, down_in, L.experts, m.s.plan, m.s.y, items, pairs, tile, -1);
}
try o.combine(m.s.y, prompt, m.s.wts, m.s.mo, n, slots_e, c.hidden);
const after = if (i + 1 < w.layers.len) w.layers[i + 1].input_norm else w.norm;
diff --git a/zig/src/families/lfm2_moe/cuda_forward.zig b/zig/src/families/lfm2_moe/cuda_forward.zig
index 6c2b7ef3577..b0ac16a93af 100644
--- a/zig/src/families/lfm2_moe/cuda_forward.zig
+++ b/zig/src/families/lfm2_moe/cuda_forward.zig
@@ -133,11 +133,11 @@ const Pass = struct {
const rows_in: cuda.experts.Rows = .{ .x = x.x, .stride = c.hidden, .slots = c.topk };
const act_in: cuda.experts.Rows = .{ .x = x.eact, .stride = c.moe_intermediate };
if (p.prompt) {
- try grouped.prompt(p.s, .up, rows_in, e.experts, plan, x.eact, items, -1);
- try grouped.prompt(p.s, .down_bf16, act_in, e.experts, plan, x.ey, items, -1);
+ try grouped.prompt(p.s, .up, rows_in, e.experts, plan, x.eact, items, pairs, tile, -1);
+ try grouped.prompt(p.s, .down_bf16, act_in, e.experts, plan, x.ey, items, pairs, tile, -1);
} else {
- try grouped.decode(p.s, .up, rows_in, e.experts, plan, x.eact, items, -1);
- try grouped.decode(p.s, .down_f32, act_in, e.experts, plan, x.ey, items, -1);
+ try grouped.decode(p.s, .up, rows_in, e.experts, plan, x.eact, items, pairs, tile, -1);
+ try grouped.decode(p.s, .down_f32, act_in, e.experts, plan, x.ey, items, pairs, tile, -1);
}
try k.moe.combine(p.s, x.ey, !p.prompt, x.wts, x.r, p.rows, c.topk, c.hidden, 0);
}
diff --git a/zig/src/families/minimax_m3/cuda_forward.zig b/zig/src/families/minimax_m3/cuda_forward.zig
index 1f1298cd30c..42fbbb2a084 100644
--- a/zig/src/families/minimax_m3/cuda_forward.zig
+++ b/zig/src/families/minimax_m3/cuda_forward.zig
@@ -61,10 +61,11 @@ pub const Modules = struct {
/// The grouped experts' scratch for a window of `rows` (allocated before serving, never mid-forward).
pub fn reserve(m: *const Modules, c: *const cfg.Config, share: anytype, rows: usize, prefill: bool) !void {
- const items = cuda.grouped.maxItems(rows * c.topk, c.experts, if (prefill) 64 else 16);
+ const pairs = rows * c.topk;
+ const tile: usize = if (prefill) 64 else 16;
const ni = share.moe.len();
- const up = try cuda.exl3_grouped.Grouped.bytes(c.hidden, ni, items, try cuda.exl3_experts.plan(c.hidden, ni, true), 2);
- const down = try cuda.exl3_grouped.Grouped.bytes(ni, c.hidden, items, try cuda.exl3_experts.plan(ni, c.hidden, false), 1);
+ const up = try cuda.exl3_grouped.Grouped.bytes(c.hidden, ni, pairs, tile, try cuda.exl3_experts.plan(c.hidden, ni, true), 2);
+ const down = try cuda.exl3_grouped.Grouped.bytes(ni, c.hidden, pairs, tile, try cuda.exl3_experts.plan(ni, c.hidden, false), 1);
const need = @max(up, down);
const g = &m.x3.grouped;
if (g.storage != null and g.storage.?.len >= need) return;
@@ -243,8 +244,8 @@ pub const Forward = struct {
const in_up: cuda.experts.Rows = .{ .x = b.x, .stride = h, .slots = topk };
const in_down: cuda.experts.Rows = .{ .x = b.eact, .stride = m.routed.width };
// one path for a prompt chunk and a window: every pick's down rows in fp32, so a row's bits never depend on it
- try g.decode(f.stream, .up, in_up, m.routed, b.plan(), b.eact, b.max_items, -1);
- try g.decode(f.stream, .down_f32, in_down, m.routed, b.plan(), b.ey, b.max_items, -1);
+ try g.decode(f.stream, .up, in_up, m.routed, b.plan(), b.eact, b.max_items, pairs, tile, -1);
+ try g.decode(f.stream, .down_f32, in_down, m.routed, b.plan(), b.ey, b.max_items, pairs, tile, -1);
try f.mlp(m.shared, f.w.share.shared.len(), R, b.sy, .f32);
try kern.moePartial(&f.m.k, f.stream, b.ey, b.wts, b.sy, f.slot(R, 2), R, topk, h);
}
diff --git a/zig/src/families/nemotron/cuda_forward.zig b/zig/src/families/nemotron/cuda_forward.zig
index 02b01c2c5b5..0bf7a6cc629 100644
--- a/zig/src/families/nemotron/cuda_forward.zig
+++ b/zig/src/families/nemotron/cuda_forward.zig
@@ -274,13 +274,13 @@ pub const Forward = struct {
const g = o.grouped();
const in = f.rowsIn(ex);
if (prompt) {
- try g.prompt(o.s, .up, in[0], ex, b.plan, b.act, items, -1);
- try g.prompt(o.s, .down_bf16, in[1], ex, b.plan, b.ymoe, items, -1);
+ try g.prompt(o.s, .up, in[0], ex, b.plan, b.act, items, pairs, tile, -1);
+ try g.prompt(o.s, .down_bf16, in[1], ex, b.plan, b.ymoe, items, pairs, tile, -1);
if (f.dump) |d| for ([_][]const u8{ "pick", "act", "ymoe" }, [_]u64{ b.pick, b.act, b.ymoe }, [_]usize{ 4, ex.width * 2, ex.dims * 2 }) |l, p, w| try d.input(o, l, p, pairs * w);
} else {
- try g.decode(o.s, .up, in[0], ex, b.plan, b.act, items, -1);
+ try g.decode(o.s, .up, in[0], ex, b.plan, b.act, items, pairs, tile, -1);
try f.mark(.experts_up);
- try g.decode(o.s, .down_f32, in[1], ex, b.plan, b.ymoe, items, -1);
+ try g.decode(o.s, .down_f32, in[1], ex, b.plan, b.ymoe, items, pairs, tile, -1);
try f.mark(.experts_down);
}
try f.sharedRest(o, m, rows, !prompt);
@@ -305,12 +305,12 @@ pub const Forward = struct {
const up_in: cuda.experts.Rows = .{ .x = b.lat, .stride = c.latent, .slots = K };
const down_in: cuda.experts.Rows = .{ .x = b.act, .stride = ex.width };
if (prompt) {
- try g.prompt(o.s, .up, up_in, ex, b.plan, b.act, items, -1);
- try g.prompt(o.s, .down_f32, down_in, ex, b.plan, b.ymoe, items, -1);
+ try g.prompt(o.s, .up, up_in, ex, b.plan, b.act, items, pairs, tile, -1);
+ try g.prompt(o.s, .down_f32, down_in, ex, b.plan, b.ymoe, items, pairs, tile, -1);
} else {
- try g.decode(o.s, .up, up_in, ex, b.plan, b.act, items, -1);
+ try g.decode(o.s, .up, up_in, ex, b.plan, b.act, items, pairs, tile, -1);
try f.mark(.experts_up);
- try g.decode(o.s, .down_f32, down_in, ex, b.plan, b.ymoe, items, -1);
+ try g.decode(o.s, .down_f32, down_in, ex, b.plan, b.ymoe, items, pairs, tile, -1);
try f.mark(.experts_down);
}
try o.latentSum(b.ymoe, b.wts, b.comb, rows, K, c.latent);
@@ -322,11 +322,11 @@ pub const Forward = struct {
const sh_up: cuda.experts.Rows = .{ .x = b.y, .stride = c.hidden, .slots = 1 };
const sh_down: cuda.experts.Rows = .{ .x = b.act, .stride = sh.width };
if (prompt) {
- try g.prompt(o.s, .up, sh_up, sh, b.plan, b.act, items, -1);
- try g.prompt(o.s, .down_f32, sh_down, sh, b.plan, b.ymoe, items, -1);
+ try g.prompt(o.s, .up, sh_up, sh, b.plan, b.act, items, rows, tile, -1);
+ try g.prompt(o.s, .down_f32, sh_down, sh, b.plan, b.ymoe, items, rows, tile, -1);
} else {
- try g.decode(o.s, .up, sh_up, sh, b.plan, b.act, items, -1);
- try g.decode(o.s, .down_f32, sh_down, sh, b.plan, b.ymoe, items, -1);
+ try g.decode(o.s, .up, sh_up, sh, b.plan, b.act, items, rows, tile, -1);
+ try g.decode(o.s, .down_f32, sh_down, sh, b.plan, b.ymoe, items, rows, tile, -1);
}
try o.addBf16(b.delta, b.delta, b.ymoe, true, rows * c.hidden);
try f.mark(.shared_wait);
@@ -373,8 +373,9 @@ pub const Forward = struct {
try sd.fork.record(o.s);
try sd.s.wait(sd.fork);
const sp = b.sharedPlan(rows);
- try g.decode(sd.s, .up, in[0], ex, sp, b.act, 2, -1);
- try g.decode(sd.s, .down_f32, in[1], ex, sp, b.ymoe, 2, -1);
+ const pairs = rows * c.slots();
+ try g.decode(sd.s, .up, in[0], ex, sp, b.act, 2, pairs, 16, -1);
+ try g.decode(sd.s, .down_f32, in[1], ex, sp, b.ymoe, 2, pairs, 16, -1);
try f.sharedRest(so, m, rows, true);
try sd.join.record(sd.s);
try f.glue.route(b.y, m.router, m.bias, b.part, b.pick, b.wts, rows, c.hidden, c.experts, c.top_k, c.slots(), c.routed_scaling, c.norm_topk);
@@ -382,9 +383,9 @@ pub const Forward = struct {
try o.planRouted(b.pick, rows, c.slots(), c.top_k, c.experts, 16, b.plan);
try f.mark(.plan);
const items = kern.maxItems(rows * c.top_k, c.experts, 16);
- try g.decode(o.s, .up, in[0], ex, b.plan, b.act, items, -1);
+ try g.decode(o.s, .up, in[0], ex, b.plan, b.act, items, pairs, 16, -1);
try f.mark(.experts_up);
- try g.decode(o.s, .down_f32, in[1], ex, b.plan, b.ymoe, items, -1);
+ try g.decode(o.s, .down_f32, in[1], ex, b.plan, b.ymoe, items, pairs, 16, -1);
try f.mark(.experts_down);
try o.s.wait(sd.join);
try f.mark(.shared_wait);
diff --git a/zig/src/families/nemotron/cuda_latent.zig b/zig/src/families/nemotron/cuda_latent.zig
index ffdb4ded492..a119d6cbf5b 100644
--- a/zig/src/families/nemotron/cuda_latent.zig
+++ b/zig/src/families/nemotron/cuda_latent.zig
@@ -339,8 +339,8 @@ fn routed(o: kern.Ops, t: Tables, s: Scratch, picks: u64, lat: u64, rows: usize,
const g = o.grouped();
try o.plan(picks, pairs, t.routed.experts, 16, s.plan);
const items = kern.maxItems(pairs, t.routed.experts, 16);
- try g.decode(o.s, .up, .{ .x = lat, .stride = c.latent, .slots = c.top_k * own }, t.routed, s.plan, s.act, items, -1);
- try g.decode(o.s, .down_f32, .{ .x = s.act, .stride = t.routed.width }, t.routed, s.plan, out, items, -1);
+ try g.decode(o.s, .up, .{ .x = lat, .stride = c.latent, .slots = c.top_k * own }, t.routed, s.plan, s.act, items, pairs, 16, -1);
+ try g.decode(o.s, .down_f32, .{ .x = s.act, .stride = t.routed.width }, t.routed, s.plan, out, items, pairs, 16, -1);
}
/// The shared expert's (row, share) pairs into `out` (partials' second region), on `s`'s fixed plan where it has one.
@@ -350,8 +350,8 @@ fn sharedRows(o: kern.Ops, t: Tables, s: Scratch, hidden: u64, rows: usize, own:
const sp = s.shared_plan orelse s.plan;
if (s.shared_plan == null) try o.plan(s.sids, rows * own, own, 16, sp);
const sh_items = kern.maxItems(rows * own, own, 16);
- try g.decode(o.s, .up, .{ .x = hidden, .stride = c.hidden, .slots = own }, t.shared, sp, s.sact, sh_items, -1);
- try g.decode(o.s, .down_f32, .{ .x = s.sact, .stride = t.shared.width }, t.shared, sp, out, sh_items, -1);
+ try g.decode(o.s, .up, .{ .x = hidden, .stride = c.hidden, .slots = own }, t.shared, sp, s.sact, sh_items, rows * own, 16, -1);
+ try g.decode(o.s, .down_f32, .{ .x = s.sact, .stride = t.shared.width }, t.shared, sp, out, sh_items, rows * own, 16, -1);
}
/// The MoE block's delta into b.delta: the other ranks' shares asked for, ours run, all summed in share order.
diff --git a/zig/src/families/nemotron/cuda_train_fwd.zig b/zig/src/families/nemotron/cuda_train_fwd.zig
index cb4ad780e00..7c0c4d30449 100644
--- a/zig/src/families/nemotron/cuda_train_fwd.zig
+++ b/zig/src/families/nemotron/cuda_train_fwd.zig
@@ -92,8 +92,8 @@ pub fn experts(f: *const fwd.Forward, m: weights.MoE, rows: usize, scale: Scale,
const items = kern.maxItems(pairs, ex.experts, tile);
const g = o.grouped();
const in = f.rowsIn(ex);
- try g.prompt(o.s, .up, in[0], ex, b.plan, b.act, items, -1);
- try g.prompt(o.s, .down_bf16, in[1], ex, b.plan, b.ymoe, items, -1);
+ try g.prompt(o.s, .up, in[0], ex, b.plan, b.act, items, pairs, tile, -1);
+ try g.prompt(o.s, .down_bf16, in[1], ex, b.plan, b.ymoe, items, pairs, tile, -1);
for (m.rest, 0..) |r, h| if (r != 0) try o.restRows(b.act, ex.width, r, b.ymoe, ex.dims, false, rows, c.slots(), c.top_k + h, ex.dims, ex.width);
const t: train_ops.Ops = .{ .k = &o.k.train, .s = o.s };
if (m.pocket) |p| try t.pocketRows(b.y, ex.dims, p.up, p.down, p.n, b.ymoe, false, rows, c.slots(), c.top_k, scale.rows, scale.from[layer]);
diff --git a/zig/src/families/qwen3_5/cuda_exl3_weights.zig b/zig/src/families/qwen3_5/cuda_exl3_weights.zig
index bf1f1c6b1d5..8e001a8ddde 100644
--- a/zig/src/families/qwen3_5/cuda_exl3_weights.zig
+++ b/zig/src/families/qwen3_5/cuda_exl3_weights.zig
@@ -33,14 +33,17 @@ pub const Kit = struct {
k.grouped = .{ .d = d, .kernels = &k.experts };
if (!g.moe()) return k; // a dense checkpoint's MLP is three Dense projections
// the grouped experts' scratch grows on first use: allocate it before any graph capture, for the widest call
- const pairs = engine.max_rows * g.slots();
- const items = cuda.grouped.maxItems(pairs, g.experts + 1, 16);
- const up = try cuda.exl3_grouped.Grouped.bytes(g.hidden, g.intermediate, items, try ex.plan(g.hidden, g.intermediate, true), 2);
- const down = try cuda.exl3_grouped.Grouped.bytes(g.intermediate, g.hidden, items, try ex.plan(g.intermediate, g.hidden, false), 1);
- k.grouped.storage = try cuda.DeviceBuffer.alloc(d, @max(up, down));
+ k.grouped.storage = try cuda.DeviceBuffer.alloc(d, try storageBytes(g));
return k;
}
+ pub fn storageBytes(g: geometry.Geometry) !usize {
+ const pairs = engine.max_rows * g.slots();
+ const up = try cuda.exl3_grouped.Grouped.bytes(g.hidden, g.intermediate, pairs, 64, try ex.plan(g.hidden, g.intermediate, true), 2);
+ const down = try cuda.exl3_grouped.Grouped.bytes(g.intermediate, g.hidden, pairs, 64, try ex.plan(g.intermediate, g.hidden, false), 1);
+ return @max(up, down);
+ }
+
pub fn deinit(k: *Kit, gpa: std.mem.Allocator) void {
k.grouped.deinit();
k.b16.unload();
diff --git a/zig/src/families/qwen3_5/cuda_routed.zig b/zig/src/families/qwen3_5/cuda_routed.zig
index 79c31dcaa16..636d1bfc673 100644
--- a/zig/src/families/qwen3_5/cuda_routed.zig
+++ b/zig/src/families/qwen3_5/cuda_routed.zig
@@ -57,11 +57,11 @@ pub fn rows(e: *E.Engine, router: u64, layer: cuda.experts.Layer, x: u64, out: u
try launch(k, e.stream, .up, rows_in, layer, plan, s.act, items, -1);
try launch(k, e.stream, .down_f32, act_in, layer, plan, s.y, items, -1);
} else if (wide) {
- try grouped.prompt(e.stream, .up, rows_in, layer, plan, s.act, items, -1);
- try grouped.prompt(e.stream, .down_bf16, act_in, layer, plan, s.y, items, -1);
+ try grouped.prompt(e.stream, .up, rows_in, layer, plan, s.act, items, pairs, tile, -1);
+ try grouped.prompt(e.stream, .down_bf16, act_in, layer, plan, s.y, items, pairs, tile, -1);
} else {
- try grouped.decode(e.stream, .up, rows_in, layer, plan, s.act, items, -1);
- try grouped.decode(e.stream, .down_f32, act_in, layer, plan, s.y, items, -1);
+ try grouped.decode(e.stream, .up, rows_in, layer, plan, s.act, items, pairs, tile, -1);
+ try grouped.decode(e.stream, .down_f32, act_in, layer, plan, s.y, items, pairs, tile, -1);
}
try e.moe.combine(e.stream, s.y, !wide or nvfp4, s.wts, out, total, slots, g.hidden, params);
}
diff --git a/zig/src/native/cuda.zig b/zig/src/native/cuda.zig
index fad11714d1f..3cb759df642 100644
--- a/zig/src/native/cuda.zig
+++ b/zig/src/native/cuda.zig
@@ -491,12 +491,12 @@ fn reasoning(comptime F: type) bool {
return if (@hasDecl(F, "reasoning")) F.reasoning else true;
}
-test "a CUDA family declares replies without reasoning with `pub const reasoning = false`; every served one keeps it" {
+test "a CUDA family declares replies without reasoning with `pub const reasoning = false`; only dots.mocr does" {
try std.testing.expect(reasoning(struct {}));
try std.testing.expect(!reasoning(struct {
pub const reasoning = false;
}));
- inline for (registry) |F| try std.testing.expect(reasoning(F));
+ inline for (registry) |F| try std.testing.expectEqual(!std.mem.eql(u8, F.model_type, "dots_ocr"), reasoning(F));
}
test "the Info openWith serves carries each family's reasoning declaration over the host's" {
diff --git a/zig/src/native/cuda_families.zig b/zig/src/native/cuda_families.zig
index bb47acc5e60..26b85155beb 100644
--- a/zig/src/native/cuda_families.zig
+++ b/zig/src/native/cuda_families.zig
@@ -14,7 +14,7 @@ const dory = @import("dory");
const qwen3 = @import("qwen3_cuda");
/// CUDA families provide metadata, open their lane backend and explain their refusals.
-pub const registry = .{ nemotron.native, nemotron.omni, glm.native, minimax.native, qwen35.native, qwen35.native_moe, flashnext.native, kolibri1.native, gemma4.native, gemma4.native31, spark25.native, dory.native, @import("lfm2_moe_cuda").native };
+pub const registry = .{ nemotron.native, nemotron.omni, glm.native, minimax.native, qwen35.native, qwen35.native_moe, flashnext.native, kolibri1.native, gemma4.native, gemma4.native31, spark25.native, dory.native, @import("dots_mocr_cuda").native, @import("lfm2_moe_cuda").native };
pub const families: []const api.Family = blk: {
var out: [registry.len + 6]api.Family = undefined;
for (registry, 0..) |F, i| out[i] = .{ .model_type = F.model_type, .formats = F.formats, .decision_formats = if (@hasDecl(F, "decision_formats")) F.decision_formats else &.{} };
@@ -38,6 +38,14 @@ test "every registered family and d1's decision engine are listed for capabiliti
try std.testing.expectEqualStrings("qwen3", families[registry.len + 4].model_type);
}
+test "dots.mocr serves its publisher's BF16 weights on CUDA through the dense engine" {
+ for (families) |f| if (std.mem.eql(u8, f.model_type, "dots_ocr")) {
+ try std.testing.expectEqual(@as(usize, 1), f.formats.len);
+ return std.testing.expectEqualStrings("bf16", f.formats[0]);
+ };
+ return error.NoDotsFamily;
+}
+
test "GLM capabilities: CUDA offers the EXL3 format its loader accepts" {
const config_json = "{\"quantization_config\":{\"quant_method\":\"exl3\",\"bits\":4,\"codebook\":\"mcg\"}}";
var why: @import("core").modelopt.Why = .{};
diff --git a/zig/tests/cuda/contract_limits.zig b/zig/tests/cuda/contract_limits.zig
index fa601d6bbd2..02f9ef56dc2 100644
--- a/zig/tests/cuda/contract_limits.zig
+++ b/zig/tests/cuda/contract_limits.zig
@@ -30,7 +30,7 @@ pub fn image(name: []const u8) error{UnknownContractImage}![]const u8 {
if (std.mem.eql(u8, name, "h3")) return &h3_image.bytes;
inline for (.{ "qwen4_projection", "qwen4_hc", "qwen4_gdn", "qwen4_ple", "gemma4_ple" }) |n|
if (std.mem.eql(u8, name, n)) return @field(kk, n);
- inline for (.{ "tree_round", "act_rows", "gdn_prep", "qk_rows", "norms", "moe", "experts", "qwen36_mtp", "nemotron_attention", "nemotron_route", "nemotron_latent", "qmm_group", "qmm_prefill", "lane_gemv", "prompt16", "qmmf", "ck_experts", "nvfp4_experts", "nvfp4_experts_tile", "nvfp4_experts32", "rank_sum", "prefill_attention", "vit", "embed_rows", "route_rows", "flashnext_gdn", "flashnext_batch", "flashnext_hc", "flashnext_attn", "flashnext_mtp", "qwen_vision", "exl3_linear", "exl3_experts", "exl3_candidate", "experts3", "experts3_prefill", "drama3", "drama3_fp8", "drama3_codec", "glm_mm", "glm_dflash", "glm_glue", "glm_ops", "glm_index", "nemotron_mamba", "sample", "qwen35", "qwen35_bf16_rows", "qwen35_keep", "qwen35_kv8", "w4a8_prefill", "qwen35_draft", "ck_lane", "gdn_train", "train_q4", "train_attn", "flashnext_train", "train", "attn_train", "qwen27_train", "tree_attention", "block_attention", "window_attention", "window_pages", "moe_glue", "dense_rows", "qmm", "flashnext_fact", "flashnext_hcf", "flashnext_fexp", "experts_rows", "nemotron_ops", "minimax_m3", "minimax_m3_attn", "moss_image", "lfm2_moe", "dots_mocr" }) |n| if (std.mem.eql(u8, name, n)) return @field(kk, n);
+ inline for (.{ "tree_round", "act_rows", "gdn_prep", "qk_rows", "norms", "moe", "experts", "qwen36_mtp", "nemotron_attention", "nemotron_route", "nemotron_latent", "qmm_group", "qmm_prefill", "lane_gemv", "prompt16", "qmmf", "ck_experts", "nvfp4_experts", "nvfp4_experts_tile", "nvfp4_experts32", "rank_sum", "prefill_attention", "vit", "embed_rows", "route_rows", "flashnext_gdn", "flashnext_batch", "flashnext_hc", "flashnext_attn", "flashnext_mtp", "qwen_vision", "exl3_linear", "exl3_experts", "exl3_candidate", "experts3", "experts3_prefill", "drama3", "drama3_fp8", "drama3_codec", "glm_mm", "glm_dflash", "glm_glue", "glm_ops", "glm_index", "nemotron_mamba", "sample", "qwen35", "qwen35_bf16_rows", "qwen35_keep", "qwen35_kv8", "w4a8_prefill", "qwen35_draft", "ck_lane", "gdn_train", "train_q4", "train_attn", "flashnext_train", "train", "attn_train", "qwen27_train", "tree_attention", "block_attention", "window_attention", "window_pages", "moe_glue", "dense_rows", "qmm", "flashnext_fact", "flashnext_hcf", "flashnext_fexp", "experts_rows", "nemotron_ops", "minimax_m3", "minimax_m3_attn", "moss_image", "lfm2_moe", "lfm2", "dots_mocr" }) |n| if (std.mem.eql(u8, name, n)) return @field(kk, n);
if (std.mem.eql(u8, name, "contract_controls")) return if (options.controls) &controls.bytes else &.{};
return error.UnknownContractImage;
}
diff --git a/zig/tests/cuda/exl3_grouped_oai_tests.zig b/zig/tests/cuda/exl3_grouped_oai_tests.zig
index f08573d4885..063e27c416d 100644
--- a/zig/tests/cuda/exl3_grouped_oai_tests.zig
+++ b/zig/tests/cuda/exl3_grouped_oai_tests.zig
@@ -76,7 +76,7 @@ fn up(r: *Rig, k: *Kit, l: cuda.experts.Layer, x: u64, pick: u64, m: usize, tile
};
try k.router.route(r.s, pick, pairs, E, tile, plan);
const out = try r.zeros(pairs * I * 2);
- try k.g.run(r.s, .up, .{ .x = x, .stride = D, .slots = SLOTS }, l, plan, out, items, -1, false);
+ try k.g.run(r.s, .up, .{ .x = x, .stride = D, .slots = SLOTS }, l, plan, out, items, pairs, tile, -1);
return r.back(out, pairs * I * 2);
}
diff --git a/zig/tests/cuda/exl3_grouped_tile_tests.zig b/zig/tests/cuda/exl3_grouped_tile_tests.zig
new file mode 100644
index 00000000000..39bb79cc44a
--- /dev/null
+++ b/zig/tests/cuda/exl3_grouped_tile_tests.zig
@@ -0,0 +1,100 @@
+//! Grouped EXL3: maxm 16 and the old maxm 64 are bit-equal; maxm 16 on a tile-64 plan drops members.
+const std = @import("std");
+const cuda = @import("cuda");
+const check = @import("check.zig");
+const quant_ref_tests = @import("quant_ref_tests.zig");
+const f64t = @import("exl3_experts_f64_tests.zig");
+
+const Rig = quant_ref_tests.RigT;
+const E = f64t.E;
+const D = f64t.D;
+const I = f64t.I;
+const SLOTS = f64t.SLOTS;
+
+const Plan = struct { p: cuda.grouped.Plan, items: usize, pairs: usize };
+const Out = struct { dev: u64, bytes: []u8 };
+
+pub fn run(gpu: check.Gpu) !void {
+ var arena = std.heap.ArenaAllocator.init(gpu.gpa);
+ defer arena.deinit();
+ var s = try cuda.Stream.init(gpu.d, false);
+ defer s.deinit();
+ var prng = std.Random.DefaultPrng.init(61);
+ var r: Rig = .{ .gpu = gpu, .a = arena.allocator(), .s = s, .rng = prng.random() };
+ defer {
+ for (r.bufs.items) |*b| b.free();
+ r.bufs.deinit(gpu.gpa);
+ }
+ var mod = try cuda.Module.load(gpu.d, cuda.kernels.experts);
+ defer mod.unload();
+ const router = try cuda.grouped.Router.resolve(mod);
+ var exp = try cuda.exl3_experts.Experts.load(gpu.d);
+ defer exp.unload();
+ const b = try f64t.buildLayer(&r, &.{ 3, 4, 5, 6, 3, 4, 5, 6, 3 });
+ const l: cuda.experts.Layer = .{ .format = .exl3, .exl3 = b.layer, .up = 0, .down = 0, .width = I, .dims = D, .experts = E };
+ var g: cuda.exl3_grouped.Grouped = .{ .d = gpu.d, .kernels = &exp };
+ defer g.deinit();
+ const grp: cuda.experts.Grouped = .{ .exl3 = &g };
+
+ for ([_]usize{ 1, 4, 16 }) |rows| {
+ const pairs = rows * SLOTS;
+ const p16 = try plan(&r, router, b.dpick, pairs, 16);
+ const p64 = try plan(&r, router, b.dpick, pairs, 64);
+ const up16 = try upBytes(&r, grp, l, b.dx, p16, 16);
+ const upOld = try upBytes(&r, grp, l, b.dx, p16, 64);
+ try check.sameBytes("up: maxm 16 vs the old maxm 64 on a tile-16 plan", upOld.bytes, up16.bytes);
+ const upT64 = try upBytes(&r, grp, l, b.dx, p64, 64);
+ try check.sameBytes("up: a tile-64 plan through decode()", upT64.bytes, up16.bytes);
+ const down16 = try downBytes(&r, grp, l, up16.dev, p16, 16);
+ const downOld = try downBytes(&r, grp, l, up16.dev, p16, 64);
+ try check.sameBytes("down: maxm 16 vs the old maxm 64 on a tile-16 plan", downOld, down16);
+ const downT64 = try downBytes(&r, grp, l, upT64.dev, p64, 64);
+ try check.sameBytes("down: a tile-64 plan through decode()", downT64, down16);
+ check.pass("exl3-grouped-tile {d} rows: maxm 16 and the old 64 bit-equal, tile-64 plan bit-equal", .{rows});
+ }
+
+ // 16 rows' 128 pairs all pick expert 0: two 64-member items, so the maxm-16 mutant drops members 16..63.
+ const skew_host = try r.a.alloc(i32, 16 * SLOTS);
+ @memset(skew_host, 0);
+ const skew = try r.dev(std.mem.sliceAsBytes(skew_host));
+ const mp = try plan(&r, router, skew, 16 * SLOTS, 64);
+ const poison_x = try r.bfs(16 * SLOTS * D, 0.5);
+ const poison_x_dev = try r.dev(std.mem.sliceAsBytes(poison_x));
+ var gm: cuda.exl3_grouped.Grouped = .{ .d = gpu.d, .kernels = &exp };
+ defer gm.deinit();
+ const gmgrp: cuda.experts.Grouped = .{ .exl3 = &gm };
+ const poisoned = try upBytes(&r, gmgrp, l, poison_x_dev, mp, 64);
+ const mutant = try upBytes(&r, gmgrp, l, b.dx, mp, 16);
+ var gc: cuda.exl3_grouped.Grouped = .{ .d = gpu.d, .kernels = &exp };
+ defer gc.deinit();
+ const real = try upBytes(&r, .{ .exl3 = &gc }, l, b.dx, mp, 64);
+ const row: usize = 16 * I * 2;
+ try check.sameBytes("planted mutant keeps the poisoned members 16..31", mutant.bytes[row..][0..row], poisoned.bytes[row..][0..row]);
+ try check.expect(!std.mem.eql(u8, mutant.bytes, real.bytes), "planted maxm 16 on a tile-64 plan dropped members and must differ from maxm 64", .{});
+ check.pass("exl3-grouped-tile planted mutant (maxm 16 on a tile-64 plan) drops members 16..63", .{});
+}
+
+fn plan(r: *Rig, router: cuda.grouped.Router, pick: u64, pairs: usize, tile: usize) !Plan {
+ const items = cuda.grouped.maxItems(pairs, E, tile);
+ const p: cuda.grouped.Plan = .{
+ .members = try r.zeros(pairs * 4 + 16),
+ .items = try r.zeros(items * 3 * 4 + 16),
+ .counts = try r.zeros(16),
+ .rank = try r.zeros(pairs * 4 + 16),
+ .hist = try r.zeros((E + 1) * 4 * ((pairs + 1023) / 1024 + 1)),
+ };
+ try router.route(r.s, pick, pairs, E, tile, p);
+ return .{ .p = p, .items = items, .pairs = pairs };
+}
+
+fn upBytes(r: *Rig, grp: cuda.experts.Grouped, l: cuda.experts.Layer, x: u64, p: Plan, tile: usize) !Out {
+ const out = try r.zeros(p.pairs * I * 2);
+ try grp.decode(r.s, .up, .{ .x = x, .stride = D, .slots = SLOTS }, l, p.p, out, p.items, p.pairs, tile, -1);
+ return .{ .dev = out, .bytes = try r.back(out, p.pairs * I * 2) };
+}
+
+fn downBytes(r: *Rig, grp: cuda.experts.Grouped, l: cuda.experts.Layer, act: u64, p: Plan, tile: usize) ![]u8 {
+ const out = try r.zeros(p.pairs * D * 4);
+ try grp.decode(r.s, .down_f32, .{ .x = act, .stride = I }, l, p.p, out, p.items, p.pairs, tile, -1);
+ return r.back(out, p.pairs * D * 4);
+}
diff --git a/zig/tests/cuda/experts3_tests.zig b/zig/tests/cuda/experts3_tests.zig
index 3c61bdeef20..7e3497976a4 100644
--- a/zig/tests/cuda/experts3_tests.zig
+++ b/zig/tests/cuda/experts3_tests.zig
@@ -195,11 +195,11 @@ fn sameBits(gpu: Gpu, k: Kernels, s: cuda.Stream, r: std.Random, sh: Shape) !voi
const rows_in: cuda.experts.Rows = .{ .x = dx.ptr, .stride = sh.dims, .slots = sh.slots };
const act_in: cuda.experts.Rows = .{ .x = act.ptr, .stride = sh.ni };
if (prompt) {
- try g.prompt(s, .up, rows_in, l, pb.plan(), act.ptr, pb.items_n, -1);
- try g.prompt(s, .down_bf16, act_in, l, pb.plan(), y.ptr, pb.items_n, -1);
+ try g.prompt(s, .up, rows_in, l, pb.plan(), act.ptr, pb.items_n, pairs, 64, -1);
+ try g.prompt(s, .down_bf16, act_in, l, pb.plan(), y.ptr, pb.items_n, pairs, 64, -1);
} else {
- try g.decode(s, .up, rows_in, l, pb.plan(), act.ptr, pb.items_n, -1);
- try g.decode(s, .down_f32, act_in, l, pb.plan(), y.ptr, pb.items_n, -1);
+ try g.decode(s, .up, rows_in, l, pb.plan(), act.ptr, pb.items_n, pairs, 16, -1);
+ try g.decode(s, .down_f32, act_in, l, pb.plan(), y.ptr, pb.items_n, pairs, 16, -1);
}
try s.synchronize();
outs[li] = .{ try check.download(gpu, act), try check.download(gpu, y) };
@@ -256,7 +256,7 @@ fn projections(gpu: Gpu, k: Kernels, s: cuda.Stream, r: std.Random) !void {
var y = try cuda.DeviceBuffer.alloc(gpu.d, pairs * n * 4);
defer y.free();
const in: cuda.experts.Rows = .{ .x = dx.ptr, .stride = kk };
- if (prompt) try g.prompt(s, .down_bf16, in, l, pb.plan(), y.ptr, pb.items_n, -1) else try g.decode(s, .down_f32, in, l, pb.plan(), y.ptr, pb.items_n, -1);
+ if (prompt) try g.prompt(s, .down_bf16, in, l, pb.plan(), y.ptr, pb.items_n, pairs, 64, -1) else try g.decode(s, .down_f32, in, l, pb.plan(), y.ptr, pb.items_n, pairs, 16, -1);
try s.synchronize();
outs[li] = try check.download(gpu, y);
for (0..pairs * n) |i| {
@@ -334,8 +334,8 @@ fn timeDecode(gpu: Gpu, k: Kernels, s: cuda.Stream, r: std.Random, reps: usize)
for (0..reps) |i| {
const p = plans[i % plans.len];
if (form < 2) {
- try g.decode(s, .up, rows_in, l3, p.plan(), act.ptr, p.items_n, -1);
- } else try g.decode(s, .down_f32, act_in, l3, p.plan(), y.ptr, p.items_n, -1);
+ try g.decode(s, .up, rows_in, l3, p.plan(), act.ptr, p.items_n, rows * sh.slots, 16, -1);
+ } else try g.decode(s, .down_f32, act_in, l3, p.plan(), y.ptr, p.items_n, rows * sh.slots, 16, -1);
}
var graph = try cuda.graph.endCapture(s);
defer graph.deinit();
diff --git a/zig/tests/cuda/fp8_experts_tests.zig b/zig/tests/cuda/fp8_experts_tests.zig
index aa74443dee0..3e50068162d 100644
--- a/zig/tests/cuda/fp8_experts_tests.zig
+++ b/zig/tests/cuda/fp8_experts_tests.zig
@@ -140,7 +140,7 @@ fn oneCase(gpu: Gpu, fx: Fixture, router: grouped.Router, g: ex_.Grouped, s: cud
const bound = grouped.maxItems(pairs, l.experts, t);
try act.fill8(0xff, s.handle); // a call that writes nothing fails rather than pass on old bytes
const up_in: ex_.Rows = .{ .x = dx.ptr, .stride = l.dims, .slots = slots };
- if (prompt) try g.prompt(s, .up, up_in, l, plan, act.ptr, bound, -1) else try g.decode(s, .up, up_in, l, plan, act.ptr, bound, -1);
+ if (prompt) try g.prompt(s, .up, up_in, l, plan, act.ptr, bound, pairs, t, -1) else try g.decode(s, .up, up_in, l, plan, act.ptr, bound, pairs, t, -1);
try s.synchronize();
const got_act = try check.download(gpu, act);
defer a.free(got_act);
@@ -153,7 +153,7 @@ fn oneCase(gpu: Gpu, fx: Fixture, router: grouped.Router, g: ex_.Grouped, s: cud
defer y.free();
try y.fill8(0xff, s.handle);
const down_in: ex_.Rows = .{ .x = act.ptr, .stride = l.width };
- if (prompt) try g.prompt(s, .down_bf16, down_in, l, plan, y.ptr, bound, -1) else try g.decode(s, .down_f32, down_in, l, plan, y.ptr, bound, -1);
+ if (prompt) try g.prompt(s, .down_bf16, down_in, l, plan, y.ptr, bound, pairs, t, -1) else try g.decode(s, .down_f32, down_in, l, plan, y.ptr, bound, pairs, t, -1);
try s.synchronize();
const got_y = try check.download(gpu, y);
defer a.free(got_y);
diff --git a/zig/tests/cuda/ggml_tests.zig b/zig/tests/cuda/ggml_tests.zig
index 849349b7074..a5cd2b28d42 100644
--- a/zig/tests/cuda/ggml_tests.zig
+++ b/zig/tests/cuda/ggml_tests.zig
@@ -150,7 +150,7 @@ fn expertCase(gpu: Gpu, fx: Fixture, router: grouped.Router, g: ex_.Grouped, k:
// up: relu^2 through experts.Grouped, then fp32 sums by the same kernels with no activation
try y.fill8(0xff, s.handle); // a call that writes nothing fails rather than pass on old bytes
const call = if (prompt) &ex_.Grouped.prompt else &ex_.Grouped.decode;
- try call(g, s, .up, .{ .x = dx.ptr, .stride = l.dims, .slots = slots }, l, plan, y.ptr, bound, -1);
+ try call(g, s, .up, .{ .x = dx.ptr, .stride = l.dims, .slots = slots }, l, plan, y.ptr, bound, pairs, tile, -1);
try s.synchronize();
try compare(gpu, try std.fmt.bufPrint(&name, "iq2 up relu^2 {s} {d} rows x {d}", .{ form, rows, slots }), y, ref_up, .relu2, tolerance(.relu2));
try y.fill8(0xff, s.handle);
@@ -160,7 +160,7 @@ fn expertCase(gpu: Gpu, fx: Fixture, router: grouped.Router, g: ex_.Grouped, k:
// down from pair rows, fp32 and bf16
inline for (.{ .{ ex_.Epi.down_f32, ggml.Out.f32 }, .{ ex_.Epi.down_bf16, ggml.Out.bf16 } }) |c| {
try y.fill8(0xff, s.handle);
- try call(g, s, c[0], .{ .x = dh.ptr, .stride = l.width }, l, plan, y.ptr, bound, -1);
+ try call(g, s, c[0], .{ .x = dh.ptr, .stride = l.width }, l, plan, y.ptr, bound, pairs, tile, -1);
try s.synchronize();
try compare(gpu, try std.fmt.bufPrint(&name, "iq2 down {t} {s} {d} rows x {d}", .{ c[1], form, rows, slots }), y, ref_down, c[1], tolerance(c[1]));
}
@@ -256,8 +256,8 @@ pub fn bench(gpu: Gpu) !void {
for (0..2) |warm| {
try t0.record(s);
for (0..reps) |_| for (ps) |p| {
- if (pi != 1) try g.decode(s, .up, .{ .x = x.ptr, .stride = dims, .slots = 9 }, l, p, h.ptr, bound, -1);
- if (pi != 0) try g.decode(s, .down_f32, .{ .x = h.ptr, .stride = width }, l, p, y.ptr, bound, -1);
+ if (pi != 1) try g.decode(s, .up, .{ .x = x.ptr, .stride = dims, .slots = 9 }, l, p, h.ptr, bound, 9, 16, -1);
+ if (pi != 0) try g.decode(s, .down_f32, .{ .x = h.ptr, .stride = width }, l, p, y.ptr, bound, 9, 16, -1);
};
try t1.record(s);
try t1.synchronize();
@@ -299,9 +299,9 @@ pub fn bench(gpu: Gpu) !void {
defer t2.deinit();
for (0..2) |warm| {
try t0.record(s);
- try g.prompt(s, .up, .{ .x = xp.ptr, .stride = dims, .slots = 9 }, l, pp, hp.ptr, pb, -1);
+ try g.prompt(s, .up, .{ .x = xp.ptr, .stride = dims, .slots = 9 }, l, pp, hp.ptr, pb, pairs, 64, -1);
try t1.record(s);
- try g.prompt(s, .down_bf16, .{ .x = hp.ptr, .stride = width }, l, pp, yp.ptr, pb, -1);
+ try g.prompt(s, .down_bf16, .{ .x = hp.ptr, .stride = width }, l, pp, yp.ptr, pb, pairs, 64, -1);
try t2.record(s);
try t2.synchronize();
if (warm == 0) continue;
diff --git a/zig/tests/cuda/main.zig b/zig/tests/cuda/main.zig
index 941b0c5a80b..0f4d6c266d6 100644
--- a/zig/tests/cuda/main.zig
+++ b/zig/tests/cuda/main.zig
@@ -457,6 +457,7 @@ fn run(gpu: check.Gpu, cmd: []const u8, rest: []const [:0]const u8) !void {
if (std.mem.eql(u8, cmd, "exlr8-dense")) return exlr8_dense_tests.run(gpu, if (rest.len > 0) rest[0] else null);
if (std.mem.eql(u8, cmd, "exlr8-bench")) return exlr8_tests.bench(gpu, try arg(rest, 0), if (rest.len > 1) rest[1] else "home", if (rest.len > 2) try std.fmt.parseInt(u16, rest[2], 10) else 40, if (rest.len > 3) try std.fmt.parseInt(usize, rest[3], 10) else 4);
if (std.mem.eql(u8, cmd, "exl3")) return exl3_tests.run(gpu, if (rest.len > 0) rest[0] else "none");
+ if (std.mem.eql(u8, cmd, "exl3-grouped-tile")) return @import("exl3_grouped_tile_tests.zig").run(gpu);
if (std.mem.eql(u8, cmd, "exl3-tiles")) return exl3_candidate_tests.run(gpu, if (rest.len > 0) rest[0] else null);
if (std.mem.eql(u8, cmd, "exl3-shapes")) return exl3_plan_bench.profileShapes(gpu, if (rest.len > 0) rest[0] else null);
if (std.mem.eql(u8, cmd, "exl3-midm")) return exl3_plan_bench.midm(gpu);
diff --git a/zig/tests/cuda/modelopt_tests.zig b/zig/tests/cuda/modelopt_tests.zig
index 42ec3e8748d..b848dcbd255 100644
--- a/zig/tests/cuda/modelopt_tests.zig
+++ b/zig/tests/cuda/modelopt_tests.zig
@@ -354,11 +354,11 @@ fn experts(gpu: Gpu, rng: std.Random, p: *const modelopt.Packer, s: cuda.Stream)
const up_in: cuda.experts.Rows = .{ .x = dx.ptr, .stride = dims, .slots = slots };
const down_in: cuda.experts.Rows = .{ .x = act.ptr, .stride = width };
if (arm == 0) {
- try g.decode(s, .up, up_in, l, plan, act.ptr, items, -1);
- try g.decode(s, .down_f32, down_in, l, plan, y.ptr, items, -1);
+ try g.decode(s, .up, up_in, l, plan, act.ptr, items, np, tile, -1);
+ try g.decode(s, .down_f32, down_in, l, plan, y.ptr, items, np, tile, -1);
} else {
- try g.prompt(s, .up, up_in, l, plan, act.ptr, items, -1);
- try g.prompt(s, .down_bf16, down_in, l, plan, y.ptr, items, -1);
+ try g.prompt(s, .up, up_in, l, plan, act.ptr, items, np, tile, -1);
+ try g.prompt(s, .down_bf16, down_in, l, plan, y.ptr, items, np, tile, -1);
}
try s.synchronize();
outs[arm] = .{ try check.download(gpu, act), try check.download(gpu, y) };
diff --git a/zig/tests/cuda/moe_live_tests.zig b/zig/tests/cuda/moe_live_tests.zig
index ab15cd4c9a5..29d0e4847d8 100644
--- a/zig/tests/cuda/moe_live_tests.zig
+++ b/zig/tests/cuda/moe_live_tests.zig
@@ -75,8 +75,8 @@ const Chain = struct {
try c.f.route(c.s, x, dims, rows, c.w, c.logits, dims, count, params);
try c.f.pick(c.s, c.logits, c.picks, c.wts, rows, routed, top_k, params);
try c.router.routeRows(c.s, c.picks, pairs, count, 16, c.plan, if (params == 0) 0 else params + 8, slots);
- try c.grouped.decode(c.s, .up, .{ .x = x, .stride = dims, .slots = slots }, c.layer, c.plan, c.act, items, -1);
- try c.grouped.decode(c.s, .down_f32, .{ .x = c.act, .stride = ni }, c.layer, c.plan, c.y, items, -1);
+ try c.grouped.decode(c.s, .up, .{ .x = x, .stride = dims, .slots = slots }, c.layer, c.plan, c.act, items, pairs, 16, -1);
+ try c.grouped.decode(c.s, .down_f32, .{ .x = c.act, .stride = ni }, c.layer, c.plan, c.y, items, pairs, 16, -1);
try c.f.combine(c.s, c.y, true, c.wts, out, rows, slots, dims, params);
}
diff --git a/zig/tests/cuda/moe_tests.zig b/zig/tests/cuda/moe_tests.zig
index b3301a13b4f..809cc2f97a5 100644
--- a/zig/tests/cuda/moe_tests.zig
+++ b/zig/tests/cuda/moe_tests.zig
@@ -339,8 +339,8 @@ fn pairExperts(gpu: Gpu, s: cuda.Stream, r: std.Random) !void {
const layer: cuda.experts.Layer = .{ .format = .affine4, .up = dpair.ptr, .down = ddown.ptr, .width = ni, .dims = dims, .experts = count, .swiglu = true };
const grouped: cuda.experts.Grouped = .{ .affine4 = &ex };
const items_n = cuda.grouped.maxItems(pairs, count, 16);
- try grouped.decode(s, .up, .{ .x = dx.ptr, .stride = dims, .slots = slots }, layer, plan, dact.ptr, items_n, -1);
- try grouped.decode(s, .down_f32, .{ .x = dact.ptr, .stride = ni }, layer, plan, dy.ptr, items_n, -1);
+ try grouped.decode(s, .up, .{ .x = dx.ptr, .stride = dims, .slots = slots }, layer, plan, dact.ptr, items_n, pairs, 16, -1);
+ try grouped.decode(s, .down_f32, .{ .x = dact.ptr, .stride = ni }, layer, plan, dy.ptr, items_n, pairs, 16, -1);
try s.synchronize();
const got = try check.download(gpu, dy);
defer a.free(got);
diff --git a/zig/tests/cuda/nvfp4_experts_tests.zig b/zig/tests/cuda/nvfp4_experts_tests.zig
index 20310178366..10f2e0a2108 100644
--- a/zig/tests/cuda/nvfp4_experts_tests.zig
+++ b/zig/tests/cuda/nvfp4_experts_tests.zig
@@ -138,7 +138,7 @@ fn oneCase(gpu: Gpu, fx: Fixture, router: grouped.Router, g: ex_.Grouped, s: cud
var act = try cuda.DeviceBuffer.alloc(gpu.d, pairs * l.width * 2);
defer act.free();
try act.fill8(0xff, s.handle); // a call that writes nothing fails rather than pass on old bytes
- try g.decode(s, .up, .{ .x = dx.ptr, .stride = l.dims, .slots = slots }, l, plan, act.ptr, bound, -1);
+ try g.decode(s, .up, .{ .x = dx.ptr, .stride = l.dims, .slots = slots }, l, plan, act.ptr, bound, pairs, 16, -1);
try s.synchronize();
const got_act = try check.download(gpu, act);
defer a.free(got_act);
@@ -150,7 +150,7 @@ fn oneCase(gpu: Gpu, fx: Fixture, router: grouped.Router, g: ex_.Grouped, s: cud
var y = try cuda.DeviceBuffer.alloc(gpu.d, pairs * l.dims * width);
defer y.free();
try y.fill8(0xff, s.handle);
- try g.decode(s, if (wide) .down_bf16 else .down_f32, .{ .x = act.ptr, .stride = l.width }, l, plan, y.ptr, bound, -1);
+ try g.decode(s, if (wide) .down_bf16 else .down_f32, .{ .x = act.ptr, .stride = l.width }, l, plan, y.ptr, bound, pairs, 16, -1);
try s.synchronize();
const got_y = try check.download(gpu, y);
defer a.free(got_y);
diff --git a/zig/tests/cuda/quant_ref_experts.zig b/zig/tests/cuda/quant_ref_experts.zig
index f841aaf3bbf..98dae8897cb 100644
--- a/zig/tests/cuda/quant_ref_experts.zig
+++ b/zig/tests/cuda/quant_ref_experts.zig
@@ -67,11 +67,11 @@ fn batch(r: *Rig, comptime label: []const u8, g: experts.Grouped, router: groupe
const up_in: experts.Rows = .{ .x = try r.dev(std.mem.sliceAsBytes(x[0 .. rows * D])), .stride = D, .slots = slots };
const down_in: experts.Rows = .{ .x = act, .stride = NI };
if (prompt) {
- try g.prompt(r.s, .up, up_in, l, pl.p, act, pl.items, -1);
- try g.prompt(r.s, .down_bf16, down_in, l, pl.p, y, pl.items, -1);
+ try g.prompt(r.s, .up, up_in, l, pl.p, act, pl.items, pairs, tile, -1);
+ try g.prompt(r.s, .down_bf16, down_in, l, pl.p, y, pl.items, pairs, tile, -1);
} else {
- try g.decode(r.s, .up, up_in, l, pl.p, act, pl.items, -1);
- try g.decode(r.s, .down_f32, down_in, l, pl.p, y, pl.items, -1);
+ try g.decode(r.s, .up, up_in, l, pl.p, act, pl.items, pairs, tile, -1);
+ try g.decode(r.s, .down_f32, down_in, l, pl.p, y, pl.items, pairs, tile, -1);
}
// gate-up: each pair's row of x through its expert, SwiGLU with the kernels' roundings
const want = try r.a.alloc(f64, pairs * NI);
@@ -140,15 +140,15 @@ fn skipped(
const up_in: experts.Rows = .{ .x = xd, .stride = D, .slots = slots };
const a0 = try r.zeros(pairs * NI * 2);
const a1 = try r.dev(fill[0 .. pairs * NI * 2]);
- try g.prompt(r.s, .up, up_in, l, pl.p, a0, pl.items, -1);
- try g.prompt(r.s, .up, up_in, l, pl.p, a1, pl.items, skip);
+ try g.prompt(r.s, .up, up_in, l, pl.p, a0, pl.items, pairs, experts.promptTile(l.format), -1);
+ try g.prompt(r.s, .up, up_in, l, pl.p, a1, pl.items, pairs, experts.promptTile(l.format), skip);
const up_plain = try r.back(a0, pairs * NI * 2);
try leftAlone(label ++ " prompt gate-up", pk, skip, up_plain, try r.back(a1, pairs * NI * 2), NI * 2, fill);
const down_in: experts.Rows = .{ .x = a0, .stride = NI };
const y0 = try r.zeros(pairs * D * 2);
const y1 = try r.dev(fill[0 .. pairs * D * 2]);
- try g.prompt(r.s, .down_bf16, down_in, l, pl.p, y0, pl.items, -1);
- try g.prompt(r.s, .down_bf16, down_in, l, pl.p, y1, pl.items, skip);
+ try g.prompt(r.s, .down_bf16, down_in, l, pl.p, y0, pl.items, pairs, experts.promptTile(l.format), -1);
+ try g.prompt(r.s, .down_bf16, down_in, l, pl.p, y1, pl.items, pairs, experts.promptTile(l.format), skip);
const down_plain = try r.back(y0, pairs * D * 2);
try leftAlone(label ++ " prompt down", pk, skip, down_plain, try r.back(y1, pairs * D * 2), D * 2, fill);
}
diff --git a/zig/tests/exl3_compile.zig b/zig/tests/exl3_compile.zig
index a3abe2e3790..e5e8468f983 100644
--- a/zig/tests/exl3_compile.zig
+++ b/zig/tests/exl3_compile.zig
@@ -21,9 +21,9 @@ export fn exl3Compile(d: *const cuda.Driver, dense: *cuda.exl3.Dense, group: *cu
linear.decodeF32(s.*, weight, .{ .x = 0 }, 0, 1) catch {};
linear.promptF32(s.*, weight, .{ .x = 0 }, 0, 1) catch {};
const grouped: cuda.experts.Grouped = .{ .exl3 = group };
- grouped.decode(s.*, .up, .{ .x = 0, .stride = l.dims }, l.*, p.*, 0, 1, -1) catch {};
- grouped.prompt(s.*, .down_f32, .{ .x = 0, .stride = l.width }, l.*, p.*, 0, 1, -1) catch {};
- grouped.prompt(s.*, .down_bf16, .{ .x = 0, .stride = l.width }, l.*, p.*, 0, 1, -1) catch {};
+ grouped.decode(s.*, .up, .{ .x = 0, .stride = l.dims }, l.*, p.*, 0, 1, 1, 16, -1) catch {};
+ grouped.prompt(s.*, .down_f32, .{ .x = 0, .stride = l.width }, l.*, p.*, 0, 1, 1, 64, -1) catch {};
+ grouped.prompt(s.*, .down_bf16, .{ .x = 0, .stride = l.width }, l.*, p.*, 0, 1, 1, 64, -1) catch {};
}
export fn candidateCompile(d: *const cuda.Driver, m: *const cuda.Module, s: *const cuda.Stream) void {