Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Each release's page on GitHub has its notes and measurements. See [the 1.0.0 rel

- Internal: 42 more lean findings fixed in the tests' comments and the tools' docstrings, with no change in behaviour (#2315).

- HIP builds and loads its runtime on Windows: `hipcc.exe` is preferred and `hipcc.bat` is found through `-Dhipcc`, `PATH` or `HIP_PATH`/`ROCM_PATH` and run through `cmd /c call`, `check-native-hip-host` compiles for the host OS instead of always `linux-gnu`, the runtime is loaded by its Windows names (`amdhip64.dll` and the versioned ones, `TENSORFOLD_HIP` overrides), the admission budget comes from the driver where `/proc/meminfo` and sysfs do not exist, the optional libraries use their Windows names (cuBLAS had been looking for `libcublas.so.13`), and the cooperative grid test runs plain where the driver reports no cooperative launch support (ROCm/HIP#3640). (#2178). Thanks to @ElderOrb.
- HIP: the decode round's residual-add and norm tail issues each row's loads before its branches, and a MoE tail loads its slots' rows together, with output bytes unchanged. On a gfx1151, one-stream decode is 2.7% faster on Qwen3.6-35B-A3B and 1.1% on the 27B. `tf_tail` is now under the kernel contract (#2247). Thanks to @AbrahamCete.
- Qwen3.8-27B on Metal: a round's keep that fails part way (an allocation, a refused keep or path) returns its error instead of aborting the process on an unended command encoder, and after a failed allocation the round can keep again; `tf-qwen27-keep-error` fails each of the keep's allocations in turn and refuses a path after the encoder has ended (#2376). Thanks to @jschmied.

Expand Down
26 changes: 24 additions & 2 deletions docs/WINDOWS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# TensorFold on Windows (native)

`tensorfold-native.exe` runs on Windows with an NVIDIA GPU through the same CUDA engine the Linux build uses. Only the
CUDA backend is built on Windows; Metal is macOS's and HIP is not built. Windows is supported from source only.
`tensorfold-native.exe` runs on Windows with an NVIDIA GPU through the same CUDA engine the Linux build uses, and with an
AMD GPU through the HIP backend when the build opts in (`-Dhip=true`). Metal is macOS's. Windows is supported from
source only.
The PR reports RTX 4090 (`x86_64-windows`, `sm_89`) and NVIDIA RTX Spark N1X (`aarch64-windows`, `sm_121`) runs.
Those device receipts have not been reproduced on the maintained tree.

Expand All @@ -28,6 +29,27 @@ zig build native install -Dsm=89 -Dstrip=true ^

`zig build test` runs the host tests without a GPU.

### HIP

Add `-Dhip=true -Dhip-arch=<arch>` and name a HIP compiler; the build takes `hipcc.exe` first and `hipcc.bat` second,
from `-Dhipcc`, `PATH`, or `HIP_PATH`/`ROCM_PATH`'s `bin`, and runs a `.bat` through `cmd /c call`:

```bat
zig build native-hip -Dhip=true -Dhip-arch=gfx1151 ^
-Dhipcc=C:\therock\gfx1151-10.1.0\bin\hipcc.exe
```

TheRock's Windows `gfx1151` dist and a HIP SDK's `hipcc.exe` both build the kernels. The server loads the runtime from
`amdhip64.dll` (the driver's copy in System32), then from a HIP SDK's `amdhip64_7.dll` and `amdhip64_6.dll` on `PATH`;
`TENSORFOLD_HIP` names one library outright, on every platform. Admission takes its budget from the driver, and the
optional libraries are found by their Windows names (`rocblas.dll`, `MIOpen.dll`, `cublas64_13.dll`).

The Windows HIP SDK does not implement cooperative launches (ROCm/HIP#3640), so a grid that asks for one runs plain; the
engine's own launches do not use them. Without a GPU, `zig build hip-host-test -Dhip=true` runs the admission tests and
`zig build check-native-hip-host -Dhip=true` compiles the real server against inert kernels. Tested on `gfx1151`
(Ryzen AI MAX+ 395): `hip-gpu-test` 38/38 on the device, and `capabilities --json` reports the HIP backend with rocBLAS
and MIOpen found.

## Run

```bat
Expand Down
46 changes: 40 additions & 6 deletions zig/build/hip.zig
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
//! HIP, built only with -Dhip=true: admission tests in `test`, GPU steps; options on every target (#1165).

const std = @import("std");
const builtin = @import("builtin");
const caps = @import("../src/hip/caps.zig");
const exlr8_build = @import("exlr8.zig");
const LazyPath = std.Build.LazyPath;

/// Some HIP SDKs ship only `hipcc.bat`, which CreateProcess cannot start: run it through `cmd /c call`.
fn hipccRun(b: *std.Build, hipcc: []const u8, args: []const []const u8) *std.Build.Step.Run {
const bat = builtin.os.tag == .windows and std.mem.endsWith(u8, hipcc, ".bat");
const run = if (bat)
b.addSystemCommand(&.{ "cmd", "/c", "call" })
else
b.addSystemCommand(&.{hipcc});
if (bat) run.addArg(hipcc);
run.addArgs(args);
return run;
}

/// The HIP compiler: `-Dhipcc`, then PATH, then `HIP_PATH`/`ROCM_PATH`'s bin (`hipcc.exe` first on Windows).
fn resolveHipcc(b: *std.Build, given: ?[]const u8) []const u8 {
if (given) |path| return b.findProgram(.{ .names = &.{path} }) orelse path;
const names: []const []const u8 = if (builtin.os.tag == .windows)
&.{ "hipcc.exe", "hipcc.bat", "hipcc" }
else
&.{"hipcc"};
if (b.findProgram(.{ .names = names })) |found| return found;
for ([_][]const u8{ "HIP_PATH", "ROCM_PATH" }) |root| {
const root_path = b.graph.environ_map.get(root) orelse continue;
for ([_][]const u8{ "hipcc.exe", "hipcc.bat" }) |name| {
const candidate = b.pathResolve(&.{ root_path, "bin", name });
if (std.Io.Dir.accessAbsolute(b.graph.io, candidate, .{})) |_| return candidate else |_| {}
}
}
return names[0];
}

/// HIP's options as given, declared whether or not -Dhip=true builds anything with them.
pub const Options = struct { hipcc: ?[]const u8, hip_include: ?[]const u8, hip_arch: ?[]const u8, device_lib: ?[]const u8, relax_branches: bool };

Expand Down Expand Up @@ -55,14 +86,14 @@ pub fn steps(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
const run_hip_tests = b.addRunArtifact(hip_tests);
test_step.dependOn(&run_hip_tests.step);
b.step("hip-host-test", "HIP admission tests without GPU work").dependOn(&run_hip_tests.step);
const hipcc = o.hipcc orelse "hipcc";
const hipcc_resolved = b.findProgram(.{ .names = &.{hipcc} }) orelse hipcc;
const hipcc_resolved = resolveHipcc(b, o.hipcc);
const hipcc = hipcc_resolved;
const hip_include = o.hip_include orelse
b.pathResolve(&.{ std.fs.path.dirname(hipcc_resolved) orelse "/opt/rocm/bin", "..", "include" });
const hip_arch = o.hip_arch orelse "gfx1151";
if (caps.Caps.of(hip_arch) == null or std.mem.indexOfScalar(u8, hip_arch, ':') != null)
std.debug.panic("-Dhip-arch {s} is not an exact name in the caps table (zig/src/hip/caps.zig)", .{hip_arch});
const hip_compile = b.addSystemCommand(&.{ hipcc, "--genco", b.fmt("--offload-arch={s}", .{hip_arch}), "-O2", "-ffp-contract=off" });
const hip_compile = hipccRun(b, hipcc, &.{ "--genco", b.fmt("--offload-arch={s}", .{hip_arch}), "-O2", "-ffp-contract=off" });
hip_compile.addFileArg(b.path("zig/kernels/hip/runtime_tests.hip"));
hip_compile.addArg("-o");
const hip_object = hip_compile.addOutputFileArg("hip-runtime-probe.hsaco");
Expand All @@ -76,7 +107,7 @@ pub fn steps(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
const hip_gpu_test = b.addTest(.{ .root_module = hip_gpu_module });
b.step("hip-gpu-build", "Compile HIP runtime tests without running GPU work").dependOn(&hip_gpu_test.step);
b.step("hip-gpu-test", "Real HIP copies, fills and architecture-selected module launches").dependOn(&b.addRunArtifact(hip_gpu_test).step);
const affine_compile = b.addSystemCommand(&.{ hipcc, "--genco", b.fmt("--offload-arch={s}", .{hip_arch}), "-O2", "-ffp-contract=off" });
const affine_compile = hipccRun(b, hipcc, &.{ "--genco", b.fmt("--offload-arch={s}", .{hip_arch}), "-O2", "-ffp-contract=off" });
affine_compile.addFileArg(b.path("zig/kernels/hip/affine.hip"));
affine_compile.addArg("-o");
const affine_image = affine_compile.addOutputFileArg("affine.hsaco");
Expand Down Expand Up @@ -163,7 +194,10 @@ pub fn steps(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
test_step.dependOn(&checksExe(b, b.graph.host, .Debug, host).step);
test_step.dependOn(&clefExe(b, b.graph.host, .Debug, host).step);
test_step.dependOn(&server(b, b.graph.host, host, build_options).step);
const check_target = b.resolveTargetQuery(.{ .cpu_arch = b.graph.host.result.cpu.arch, .os_tag = .linux, .abi = .gnu });
const check_target = if (b.graph.host.result.os.tag == .windows)
b.graph.host
else
b.resolveTargetQuery(.{ .cpu_arch = b.graph.host.result.cpu.arch, .os_tag = .linux, .abi = .gnu });
const check = engineModules(b, check_target, .Debug, stub_kernels, null);
b.step("check-native-hip-host", "Compile the real native HIP server with inert kernels, without GPU execution").dependOn(&server(b, check_target, check, build_options).step);
const gpu = engineModules(b, target, optimize, objects, hip_include);
Expand Down Expand Up @@ -469,7 +503,7 @@ pub fn compile(b: *std.Build, cc: Hipcc, arch: []const u8, source: std.Build.Laz
fn compileSource(b: *std.Build, cc: Hipcc, arch: []const u8, source: LazyPath, out: []const u8, strict: bool) LazyPath {
const c = caps.Caps.of(arch).?;
const root = std.fs.path.dirname(std.fs.path.dirname(cc.resolved) orelse ".") orelse ".";
const run = b.addSystemCommand(&.{ cc.name, "--genco" });
const run = hipccRun(b, cc.name, &.{"--genco"});
run.addArgs(&flags);
if (strict) run.addArgs(&.{ "-O2", "-fno-fast-math", "-fdenormal-fp-math=ieee" });
run.addArg(b.fmt("-DTF_WAVE={d}", .{c.wave}));
Expand Down
2 changes: 1 addition & 1 deletion zig/runtime_library_host_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ test "runtime library: discovery uses overrides and retains found libraries with
pub const DynLib = struct {
pub fn open(path: []const u8) !@This() {
attempts += 1;
if (std.mem.eql(u8, path, "libnccl.so.2") or std.mem.eql(u8, path, libs.spec(.jpeg).names[0])) return .{};
if (std.mem.eql(u8, path, libs.spec(.nccl).names[0]) or std.mem.eql(u8, path, libs.spec(.jpeg).names[0])) return .{};
return error.FileNotFound;
}
pub fn close(_: *@This()) void {
Expand Down
5 changes: 3 additions & 2 deletions zig/src/families/nemotron/hip/engine.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Nemotron-H on one HIP device: the kernels, a stream, the rank's weights, the KV pool and the scratch.
const std = @import("std");
const platform = @import("sys");
const hip = @import("hip");
const config = @import("../config.zig");
const tensors = @import("tensors.zig");
Expand Down Expand Up @@ -383,8 +384,8 @@ const Body = struct {
};

fn nowNs() u64 {
var ts: std.c.timespec = undefined;
_ = std.c.clock_gettime(.MONOTONIC, &ts);
var ts: platform.c.timespec = undefined;
_ = platform.c.clock_gettime(platform.c.CLOCK.MONOTONIC, &ts);
return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
}

Expand Down
16 changes: 13 additions & 3 deletions zig/src/hip/admission.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
//! What the HIP engine may serve: the CUDA path's memory rules (cuda_memory.zig) and the window and weights it reads.
const std = @import("std");
const builtin = @import("builtin");
const memory = @import("cuda_memory");
const Context = @import("context.zig").Context;
const Allocator = std.mem.Allocator;

/// Windows has no /proc/meminfo and no sysfs VRAM file; the driver's own count is the budget.
const windows = builtin.os.tag == .windows;

pub const MemInfo = memory.MemInfo;
pub const Pool = memory.Pool;
pub const meminfo = memory.meminfo;
Expand Down Expand Up @@ -51,13 +55,19 @@ pub fn budget(gpa: Allocator, io: std.Io, ctx: Context) !Pool {
const integrated = (try ctx.attribute(.integrated)) != 0;
const card = try ctx.memInfo();
const own: MemInfo = .{ .total = card.total, .available = card.free };
const vram = if (integrated) vramBytes(gpa, io, ctx) else null;
const text = if (sourceOf(integrated, own, vram) == .host) procText(gpa, io, "/proc/meminfo") else null;
defer if (text) |t| gpa.free(t);
const env: Env = .{
.reserve = getenv("TENSORFOLD_MEMORY_RESERVE_GIB"),
.limit = getenv("TENSORFOLD_HIP_MEMORY_LIMIT_GB"),
};
if (windows) {
// WDDM reports the whole budget through the driver: no host text, no dedicated-VRAM file.
const p = try plan(false, own, null, null, env);
announce(p);
return p.pool;
}
const vram = if (integrated) vramBytes(gpa, io, ctx) else null;
const text = if (sourceOf(integrated, own, vram) == .host) procText(gpa, io, "/proc/meminfo") else null;
defer if (text) |t| gpa.free(t);
const p = try plan(integrated, own, text, vram, env);
announce(p);
return p.pool;
Expand Down
5 changes: 3 additions & 2 deletions zig/src/hip/kernel_tests/nemotron/epilogue.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Nemotron-H's relu^2 as the stream tile's epilogue against the product and tf_nemo_relu2: the same bytes.

const std = @import("std");
const platform = @import("sys");
const rig = @import("../rig.zig");
const fx = @import("fixture.zig");
const nm = @import("../../moe_nemotron.zig");
Expand Down Expand Up @@ -84,8 +85,8 @@ test "the relu^2 epilogue writes the bytes of the product and tf_nemo_relu2: the
}

fn nowNs() u64 {
var ts: std.c.timespec = undefined;
_ = std.c.clock_gettime(.MONOTONIC, &ts);
var ts: platform.c.timespec = undefined;
_ = platform.c.clock_gettime(platform.c.CLOCK.MONOTONIC, &ts);
return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
}

Expand Down
5 changes: 3 additions & 2 deletions zig/src/hip/kernel_tests/nemotron/fused.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Nemotron-H's merged decode launches against the launches they replace: the same bytes, every shape they take.

const std = @import("std");
const platform = @import("sys");
const rig = @import("../rig.zig");
const DeviceBuffer = @import("../../memory.zig").DeviceBuffer;
const moe_launch = @import("../../launches/nemotron_moe.zig");
Expand Down Expand Up @@ -203,8 +204,8 @@ test "the residual and the norm in one launch write the bytes of tf_add (or tf_a
}

fn nowNs() u64 {
var ts: std.c.timespec = undefined;
_ = std.c.clock_gettime(.MONOTONIC, &ts);
var ts: platform.c.timespec = undefined;
_ = platform.c.clock_gettime(platform.c.CLOCK.MONOTONIC, &ts);
return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
}

Expand Down
5 changes: 3 additions & 2 deletions zig/src/hip/kernel_tests/nemotron/pick.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Nemotron-H's 512-thread pick by rank against the wave a pick at a time: the same bytes, and their times.

const std = @import("std");
const platform = @import("sys");
const rig = @import("../rig.zig");
const launch = @import("../../launches/nemotron_moe.zig");
const DeviceBuffer = @import("../../memory.zig").DeviceBuffer;
Expand Down Expand Up @@ -90,8 +91,8 @@ const Case = struct {
};

fn nowNs() u64 {
var ts: std.c.timespec = undefined;
_ = std.c.clock_gettime(.MONOTONIC, &ts);
var ts: platform.c.timespec = undefined;
_ = platform.c.clock_gettime(platform.c.CLOCK.MONOTONIC, &ts);
return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
}

Expand Down
5 changes: 3 additions & 2 deletions zig/src/hip/kernel_tests/nemotron/window.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Nemotron's Mamba window launches against the mixer a row at a time, each state copied out but the last.

const std = @import("std");
const platform = @import("sys");
const rig = @import("../rig.zig");
const raw = @import("../../raw.zig");
const DeviceBuffer = @import("../../memory.zig").DeviceBuffer;
Expand Down Expand Up @@ -169,8 +170,8 @@ test "the Mamba window's two launches write the bytes of the mixer a row at a ti
}

fn nowNs() u64 {
var ts: std.c.timespec = undefined;
_ = std.c.clock_gettime(.MONOTONIC, &ts);
var ts: platform.c.timespec = undefined;
_ = platform.c.clock_gettime(platform.c.CLOCK.MONOTONIC, &ts);
return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
}

Expand Down
16 changes: 15 additions & 1 deletion zig/src/hip/runtime.zig
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
//! Complete model-free HIP runtime admission; partial symbol tables never escape.
const std = @import("std");
const builtin = @import("builtin");
const platform = @import("sys");
const abi = @import("abi.zig");
pub const Error = error{ DriverUnavailable, MissingSymbol, HipFailed, Invalid };

/// The runtime's file names, most specific first: Windows' unversioned DLL, then the SDK's versioned copies.
const names: []const []const u8 = if (builtin.os.tag == .windows)
&.{ "amdhip64.dll", "amdhip64_7.dll", "amdhip64_6.dll" }
else
&.{"libamdhip64.so"};

pub const Runtime = struct {
lib: platform.DynLib,
api: abi.Api,

pub fn open() Error!Runtime {
return openPath("libamdhip64.so");
if (platform.getenv("TENSORFOLD_HIP")) |path| return openPath(path);
for (names) |name| {
return openPath(name) catch |err| switch (err) {
error.DriverUnavailable => continue,
else => return err,
};
}
return error.DriverUnavailable;
}

pub fn openPath(path: []const u8) Error!Runtime {
Expand Down
5 changes: 3 additions & 2 deletions zig/src/hip/runtime_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ test "a cooperative grid of one block a compute unit, and the device's attribute
var name_buf: [256]u8 = undefined;
try std.testing.expect((try ctx.name(&name_buf)).len > 0);
try std.testing.expectEqual(@as(c_int, 32), try ctx.attribute(.warp_size));
try std.testing.expect(try ctx.attribute(.cooperative_launch) != 0);
// Windows HIP has no cooperative launch support (ROCm/HIP#3640), so the grid runs plain there.
const cooperative = (try ctx.attribute(.cooperative_launch)) != 0;
const cus: u32 = @intCast(try ctx.attribute(.multiprocessor_count));
var arch_buffer: [256]u8 = undefined;
var m = try Module.loadForArchitecture(&r, &.{probe_image}, try @import("device_arch.zig").query(&r, 0, &arch_buffer));
Expand All @@ -287,7 +288,7 @@ test "a cooperative grid of one block a compute unit, and the device's attribute
try args.add(y.ptr);
try args.add(@as(f32, 3));
try args.add(cus * 256);
try launch.launch(fill, .{ .grid = .{ .x = cus }, .block = .{ .x = 256 }, .cooperative = true }, stream, &args);
try launch.launch(fill, .{ .grid = .{ .x = cus }, .block = .{ .x = 256 }, .cooperative = cooperative }, stream, &args);
try stream.synchronize();
const got = try std.testing.allocator.alloc(f32, cus * 256);
defer std.testing.allocator.free(got);
Expand Down
5 changes: 3 additions & 2 deletions zig/src/hip/tuning/search.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! hip-affine-tune's proposal: prompt table rows from timed cells against gfx1100's choices; no device, host-tested.

const std = @import("std");
const platform = @import("sys");
const registry = @import("core").registry;
const tuning = @import("tuning.zig");
const affine = @import("../launches/affine.zig");
Expand Down Expand Up @@ -333,8 +334,8 @@ fn dist(a: f64, b: f64) f64 {
}

fn nowNs() u64 {
var ts: std.c.timespec = undefined;
_ = std.c.clock_gettime(.MONOTONIC, &ts);
var ts: platform.c.timespec = undefined;
_ = platform.c.clock_gettime(platform.c.CLOCK.MONOTONIC, &ts);
return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
}

Expand Down
Loading
Loading