const std = @import("std"); const builtin = @import("builtin"); /// NVML Dynamic Loader for CLI /// Pure Zig implementation using dlopen/LoadLibrary /// No build-time dependency on NVIDIA SDK // Platform-specific dynamic loading const DynLib = switch (builtin.os.tag) { .windows => struct { handle: std.os.windows.HMODULE, fn open(path: []const u8) !@This() { const wide_path = try std.os.windows.sliceToPrefixedFileW(path); const handle = std.os.windows.LoadLibraryW(&wide_path.data) orelse return error.LibraryNotFound; return .{ .handle = handle }; } fn close(self: *@This()) void { _ = std.os.windows.FreeLibrary(self.handle); } fn lookup(self: @This(), name: []const u8) ?*anyopaque { return std.os.windows.GetProcAddress(self.handle, name); } }, else => struct { handle: *anyopaque, // Extern declarations for dlopen/dlsym extern "c" fn dlopen(pathname: [*:0]const u8, mode: c_int) ?*anyopaque; extern "c" fn dlsym(handle: *anyopaque, symbol: [*:0]const u8) ?*anyopaque; extern "c" fn dlclose(handle: *anyopaque) c_int; const RTLD_NOW = 2; fn open(path: []const u8) !@This() { const c_path = try std.cstr.addNullByte(std.heap.c_allocator, path); defer std.heap.c_allocator.free(c_path); const handle = dlopen(c_path.ptr, RTLD_NOW) orelse return error.LibraryNotFound; return .{ .handle = handle }; } fn close(self: *@This()) void { _ = dlclose(self.handle); } fn lookup(self: @This(), name: []const u8) ?*anyopaque { const c_name = std.cstr.addNullByte(std.heap.c_allocator, name) catch return null; defer std.heap.c_allocator.free(c_name); return dlsym(self.handle, c_name.ptr); } }, }; // NVML type definitions (mirrors nvml.h) pub const nvmlReturn_t = c_int; pub const nvmlDevice_t = *anyopaque; pub const nvmlUtilization_t = extern struct { gpu: c_uint, memory: c_uint, }; pub const nvmlMemory_t = extern struct { total: c_ulonglong, free: c_ulonglong, used: c_ulonglong, }; // NVML constants const NVML_SUCCESS = 0; const NVML_TEMPERATURE_GPU = 0; const NVML_CLOCK_SM = 0; const NVML_CLOCK_MEM = 1; // NVML function types const nvmlInit_v2_fn = *const fn () callconv(.C) nvmlReturn_t; const nvmlShutdown_fn = *const fn () callconv(.C) nvmlReturn_t; const nvmlDeviceGetCount_fn = *const fn (*c_uint) callconv(.C) nvmlReturn_t; const nvmlDeviceGetHandleByIndex_v2_fn = *const fn (c_uint, *nvmlDevice_t) callconv(.C) nvmlReturn_t; const nvmlDeviceGetName_fn = *const fn (nvmlDevice_t, [*]u8, c_uint) callconv(.C) nvmlReturn_t; const nvmlDeviceGetUtilizationRates_fn = *const fn (nvmlDevice_t, *nvmlUtilization_t) callconv(.C) nvmlReturn_t; const nvmlDeviceGetMemoryInfo_fn = *const fn (nvmlDevice_t, *nvmlMemory_t) callconv(.C) nvmlReturn_t; const nvmlDeviceGetTemperature_fn = *const fn (nvmlDevice_t, c_uint, *c_uint) callconv(.C) nvmlReturn_t; const nvmlDeviceGetPowerUsage_fn = *const fn (nvmlDevice_t, *c_uint) callconv(.C) nvmlReturn_t; const nvmlDeviceGetClockInfo_fn = *const fn (nvmlDevice_t, c_uint, *c_uint) callconv(.C) nvmlReturn_t; const nvmlDeviceGetUUID_fn = *const fn (nvmlDevice_t, [*]u8, c_uint) callconv(.C) nvmlReturn_t; const nvmlDeviceGetVbiosVersion_fn = *const fn (nvmlDevice_t, [*]u8, c_uint) callconv(.C) nvmlReturn_t; /// GPU information structure pub const GPUInfo = struct { index: u32, name: [256:0]u8, utilization: u32, memory_used: u64, memory_total: u64, temperature: u32, power_draw: u32, clock_sm: u32, clock_memory: u32, uuid: [64:0]u8, vbios_version: [32:0]u8, }; /// NVML handle with loaded functions pub const NVML = struct { lib: DynLib, available: bool, // Function pointers init: nvmlInit_v2_fn, shutdown: nvmlShutdown_fn, get_count: nvmlDeviceGetCount_fn, get_handle_by_index: nvmlDeviceGetHandleByIndex_v2_fn, get_name: ?nvmlDeviceGetName_fn, get_utilization: ?nvmlDeviceGetUtilizationRates_fn, get_memory: ?nvmlDeviceGetMemoryInfo_fn, get_temperature: ?nvmlDeviceGetTemperature_fn, get_power_usage: ?nvmlDeviceGetPowerUsage_fn, get_clock: ?nvmlDeviceGetClockInfo_fn, get_uuid: ?nvmlDeviceGetUUID_fn, get_vbios: ?nvmlDeviceGetVbiosVersion_fn, last_error: [256:0]u8, /// Load NVML dynamically pub fn load() !?NVML { var nvml: NVML = undefined; // Try platform-specific library names const lib_names = switch (builtin.os.tag) { .windows => &[_][]const u8{ "nvml.dll", "C:\\Windows\\System32\\nvml.dll", }, .linux => &[_][]const u8{ "libnvidia-ml.so.1", "libnvidia-ml.so", }, else => return null, // NVML not supported on other platforms }; // Try to load library var loaded = false; for (lib_names) |name| { if (DynLib.open(name)) |lib| { nvml.lib = lib; loaded = true; break; } else |_| continue; } if (!loaded) { return null; // NVML not available (no NVIDIA driver) } // Load required functions nvml.init = @ptrCast(nvml.lib.lookup("nvmlInit_v2") orelse return error.InitNotFound); nvml.shutdown = @ptrCast(nvml.lib.lookup("nvmlShutdown") orelse return error.ShutdownNotFound); nvml.get_count = @ptrCast(nvml.lib.lookup("nvmlDeviceGetCount") orelse return error.GetCountNotFound); nvml.get_handle_by_index = @ptrCast(nvml.lib.lookup("nvmlDeviceGetHandleByIndex_v2") orelse return error.GetHandleNotFound); // Load optional functions nvml.get_name = @ptrCast(nvml.lib.lookup("nvmlDeviceGetName")); nvml.get_utilization = @ptrCast(nvml.lib.lookup("nvmlDeviceGetUtilizationRates")); nvml.get_memory = @ptrCast(nvml.lib.lookup("nvmlDeviceGetMemoryInfo")); nvml.get_temperature = @ptrCast(nvml.lib.lookup("nvmlDeviceGetTemperature")); nvml.get_power_usage = @ptrCast(nvml.lib.lookup("nvmlDeviceGetPowerUsage")); nvml.get_clock = @ptrCast(nvml.lib.lookup("nvmlDeviceGetClockInfo")); nvml.get_uuid = @ptrCast(nvml.lib.lookup("nvmlDeviceGetUUID")); nvml.get_vbios = @ptrCast(nvml.lib.lookup("nvmlDeviceGetVbiosVersion")); // Initialize NVML const result = nvml.init(); if (result != NVML_SUCCESS) { nvml.setError("NVML initialization failed"); nvml.lib.close(); return error.NVMLInitFailed; } nvml.available = true; return nvml; } /// Unload NVML pub fn unload(self: *NVML) void { if (self.available) { _ = self.shutdown(); } self.lib.close(); } /// Check if NVML is available pub fn isAvailable(self: NVML) bool { return self.available; } /// Get last error message pub fn getLastError(self: NVML) []const u8 { return std.mem.sliceTo(&self.last_error, 0); } fn setError(self: *NVML, msg: []const u8) void { @memset(&self.last_error, 0); const len = @min(msg.len, self.last_error.len - 1); @memcpy(self.last_error[0..len], msg[0..len]); } /// Get number of GPUs pub fn getGPUCount(self: *NVML) !u32 { var count: c_uint = 0; const result = self.get_count(&count); if (result != NVML_SUCCESS) { self.setError("Failed to get GPU count"); return error.GetCountFailed; } return @intCast(count); } /// Get GPU info by index pub fn getGPUInfo(self: *NVML, index: u32) !GPUInfo { var info: GPUInfo = .{ .index = index, .name = std.mem.zeroes([256:0]u8), .utilization = 0, .memory_used = 0, .memory_total = 0, .temperature = 0, .power_draw = 0, .clock_sm = 0, .clock_memory = 0, .uuid = std.mem.zeroes([64:0]u8), .vbios_version = std.mem.zeroes([32:0]u8), }; var device: nvmlDevice_t = undefined; var result = self.get_handle_by_index(index, &device); if (result != NVML_SUCCESS) { self.setError("Failed to get device handle"); return error.GetHandleFailed; } // Get name if (self.get_name) |func| { _ = func(device, &info.name, @sizeOf(@TypeOf(info.name))); } // Get utilization if (self.get_utilization) |func| { var util: nvmlUtilization_t = undefined; result = func(device, &util); if (result == NVML_SUCCESS) { info.utilization = @intCast(util.gpu); } } // Get memory if (self.get_memory) |func| { var mem: nvmlMemory_t = undefined; result = func(device, &mem); if (result == NVML_SUCCESS) { info.memory_used = mem.used; info.memory_total = mem.total; } } // Get temperature if (self.get_temperature) |func| { var temp: c_uint = 0; result = func(device, NVML_TEMPERATURE_GPU, &temp); if (result == NVML_SUCCESS) { info.temperature = @intCast(temp); } } // Get power usage if (self.get_power_usage) |func| { var power: c_uint = 0; result = func(device, &power); if (result == NVML_SUCCESS) { info.power_draw = @intCast(power); } } // Get clocks if (self.get_clock) |func| { var clock: c_uint = 0; result = func(device, NVML_CLOCK_SM, &clock); if (result == NVML_SUCCESS) { info.clock_sm = @intCast(clock); } result = func(device, NVML_CLOCK_MEM, &clock); if (result == NVML_SUCCESS) { info.clock_memory = @intCast(clock); } } // Get UUID if (self.get_uuid) |func| { _ = func(device, &info.uuid, @sizeOf(@TypeOf(info.uuid))); } // Get VBIOS version if (self.get_vbios) |func| { _ = func(device, &info.vbios_version, @sizeOf(@TypeOf(info.vbios_version))); } return info; } /// Get info for all GPUs pub fn getAllGPUInfo(self: *NVML, allocator: std.mem.Allocator) ![]GPUInfo { const count = try self.getGPUCount(); if (count == 0) return &[_]GPUInfo{}; var gpus = try allocator.alloc(GPUInfo, count); errdefer allocator.free(gpus); for (0..count) |i| { gpus[i] = try self.getGPUInfo(@intCast(i)); } return gpus; } }; // Convenience functions for simple use cases /// Quick check if NVML is available (creates and destroys temporary handle) pub fn isNVMLAvailable() bool { if (NVML.load()) |maybe_nvml| { if (maybe_nvml) |nvml| { var nvml_mut = nvml; defer nvml_mut.unload(); return nvml_mut.isAvailable(); } } else |_| {} return false; } /// Format GPU info as string for display pub fn formatGPUInfo(allocator: std.mem.Allocator, gpus: []const GPUInfo) ![]u8 { var buf = std.ArrayList(u8).init(allocator); defer buf.deinit(); const writer = buf.writer(); try writer.writeAll("GPU Status (NVML)\n"); try writer.writeAll("═" ** 50); try writer.writeAll("\n\n"); for (gpus) |gpu| { const name = std.mem.sliceTo(&gpu.name, 0); try writer.print("GPU {d}: {s}\n", .{ gpu.index, name }); try writer.print("\tUtilization: {d}%\n", .{gpu.utilization}); try writer.print("\tMemory: {d}/{d} MB\n", .{ gpu.memory_used / 1024 / 1024, gpu.memory_total / 1024 / 1024, }); try writer.print("\tTemperature: {d}°C\n", .{gpu.temperature}); if (gpu.power_draw > 0) { try writer.print("\tPower: {d:.1} W\n", .{@as(f64, @floatFromInt(gpu.power_draw)) / 1000.0}); } if (gpu.clock_sm > 0) { try writer.print("\tSM Clock: {d} MHz\n", .{gpu.clock_sm}); } try writer.writeAll("\n"); } return buf.toOwnedSlice(); }