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
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,27 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libjansson-dev libncursesw5-dev nvidia-cuda-toolkit
sudo apt-get install -y build-essential libjansson-dev libncursesw5-dev

- name: Compile check
run: make check

# The runner has no NVIDIA driver. Link against a stub libnvidia-ml.so.1
# that defines exactly the symbols include/nvml_api.h declares, so the
# -l:libnvidia-ml.so.1 link line and the declared symbol set are both
# exercised, not just compilation.
- name: Link against stub NVML
run: |
mkdir -p build/stub
cc -shared -fPIC -Wall -Wextra -Werror -Wl,-soname,libnvidia-ml.so.1 \
-Iinclude -o build/stub/libnvidia-ml.so.1 tests/nvml_stub.c
make NVML_LIBDIR=build/stub
test -x build/nvfd

shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run ShellCheck
run: find scripts -name '*.sh' -exec shellcheck {} +
run: find scripts tests -name '*.sh' -exec shellcheck {} +
29 changes: 21 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ BINDIR = $(PREFIX)/bin
CONFDIR = /etc/nvfd
UNITDIR = /etc/systemd/system

# NVIDIA CUDA paths (try standard locations)
CUDA_PATH ?= $(shell [ -d /usr/local/cuda ] && echo /usr/local/cuda || echo /usr)
CFLAGS += -I$(CUDA_PATH)/include -Iinclude
LDFLAGS += -L$(CUDA_PATH)/lib64

LIBS = -lnvidia-ml -ljansson -lncursesw
# NVML declarations are carried in include/nvml_api.h, so no CUDA toolkit is
# needed to build. Any NVML call that is not declared there is a hard error
# rather than an implicit declaration.
CFLAGS += -Iinclude -Werror=implicit-function-declaration

# Link the driver's NVML by SONAME: libnvidia-ml.so.1 ships with every
# driver, whereas the unversioned libnvidia-ml.so symlink only comes with
# -dev packages or the CUDA toolkit. Some distributions keep it outside
# ld's default search path, so use the directory registered with ldconfig.
LDCONFIG ?= /sbin/ldconfig
NVML_CACHE_PATTERN = libnvidia-ml\.so\.1 \(libc6,
NVML_LIBDIR ?= $(shell $(LDCONFIG) -p 2>/dev/null | awk '/$(NVML_CACHE_PATTERN)/{print $$NF; exit}' | xargs -r dirname)
LDFLAGS += $(if $(NVML_LIBDIR),-L$(NVML_LIBDIR))
LIBS = -l:libnvidia-ml.so.1 -ljansson -lncursesw

SRCDIR = src
BUILDDIR = build
Expand All @@ -21,7 +29,7 @@ SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
TARGET = $(BUILDDIR)/nvfd

.PHONY: all clean check install uninstall install-utils uninstall-utils
.PHONY: all clean check test install uninstall install-utils uninstall-utils

all: $(TARGET)

Expand All @@ -34,9 +42,14 @@ $(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
$(BUILDDIR):
mkdir -p $(BUILDDIR)

check: $(OBJS)
check: $(OBJS) test
@echo "All source files compiled successfully."

test:
sh tests/test_makefile_nvml_detection.sh
sh tests/test_nvml_api_declarations.sh
sh tests/test_find_nvml.sh

clean:
rm -rf $(BUILDDIR)

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Recommendations:
- Linux operating system
- `libjansson-dev` — JSON library
- `libncursesw5-dev` — ncurses wide-character support
- NVML headers (included with CUDA toolkit or `nvidia-cuda-toolkit` package)
- NVIDIA driver R520 or newer — NVFD links against the driver's `libnvidia-ml.so.1`; no CUDA toolkit or NVML headers are needed

## Installation

Expand Down Expand Up @@ -329,7 +329,7 @@ The utility script requires:
- `nvidia-smi` (included with NVIDIA drivers)
- `nvfd` binary (installed via this package)

No CUDA toolkit required for runtime.
No CUDA toolkit is required, either to build NVFD or at runtime.

## Migration from v1.x

Expand Down
4 changes: 2 additions & 2 deletions README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ NVFD 是一款開源的 Linux NVIDIA GPU 風扇控制守護程式。透過 NVML
- Linux 作業系統
- `libjansson-dev` — JSON 函式庫
- `libncursesw5-dev` — ncurses 寬字元支援
- NVML 標頭檔(包含在 CUDA toolkit 或 `nvidia-cuda-toolkit` 套件中)
- NVIDIA 驅動程式 R520 或更新版本 — NVFD 直接連結驅動程式附帶的 `libnvidia-ml.so.1`,不需要 CUDA toolkit 或 NVML 標頭檔

## 安裝

Expand Down Expand Up @@ -329,7 +329,7 @@ ExecStart=/usr/local/bin/nvfd-fan-control.sh --threshold-up 50 --threshold-down
- `nvidia-smi`(包含在 NVIDIA 驅動中)
- `nvfd` 二進位檔案(通過此套件安裝)

運行時不需要 CUDA toolkit。
建置與運行皆不需要 CUDA toolkit。

## 從 v1.x 遷移

Expand Down
2 changes: 1 addition & 1 deletion include/nvfd.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#ifndef NVFD_H
#define NVFD_H

#include <nvml.h>
#include "nvml_api.h"
#include <signal.h>

#define NVFD_VERSION "1.1"
Expand Down
98 changes: 98 additions & 0 deletions include/nvml_api.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Declarations for the NVML entry points nvfd uses.
*
* nvfd deliberately does not include NVIDIA's nvml.h. That header ships only
* with the CUDA toolkit, and installing the toolkit through a distribution
* package can replace or pin the NVIDIA driver on the host. The NVML ABI is
* stable and versioned by symbol name (the _v2 suffixes below), and
* libnvidia-ml.so.1 ships with every driver, so declaring the handful of
* functions we call is sufficient. Signatures and enum values match nvml.h
* from CUDA 12.x; they are part of the ABI and do not change.
*
* Minimum driver: R520, the first branch whose libnvidia-ml.so.1 exports
* every symbol below. nvmlDeviceSetFanSpeed_v2 / SetDefaultFanSpeed_v2
* appeared in R515; nvmlDeviceSetFanControlPolicy in 520.61.05 (backported
* to 515.105.01). Per `nm -D` on the NVIDIA rhel8 repo builds of 515.43.04,
* 515.105.01 and 520.61.05.
*/
#ifndef NVFD_NVML_API_H
#define NVFD_NVML_API_H

#ifdef __cplusplus
extern "C" {
#endif

typedef struct nvmlDevice_st *nvmlDevice_t;

/* Only NVML_SUCCESS is compared against; every other code is passed straight
* to nvmlErrorString(). NVML_ERROR_UNKNOWN is listed so the enum has the full
* value range nvml.h gives it. */
typedef enum nvmlReturn_enum {
NVML_SUCCESS = 0,
NVML_ERROR_UNKNOWN = 999
} nvmlReturn_t;

typedef enum nvmlEnableState_enum {
NVML_FEATURE_DISABLED = 0,
NVML_FEATURE_ENABLED = 1
} nvmlEnableState_t;

typedef enum nvmlTemperatureSensors_enum {
NVML_TEMPERATURE_GPU = 0
} nvmlTemperatureSensors_t;

typedef unsigned int nvmlFanControlPolicy_t;
/* sic — NVIDIA's spelling */
#define NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW 0
#define NVML_FAN_POLICY_MANUAL 1

typedef struct nvmlUtilization_st {
unsigned int gpu;
unsigned int memory;
} nvmlUtilization_t;

/* v1 layout, as consumed by nvmlDeviceGetMemoryInfo (not the _v2 variant). */
typedef struct nvmlMemory_st {
unsigned long long total;
unsigned long long free;
unsigned long long used;
} nvmlMemory_t;

#define NVML_DEVICE_NAME_BUFFER_SIZE 64

nvmlReturn_t nvmlInit_v2(void);
nvmlReturn_t nvmlShutdown(void);
const char *nvmlErrorString(nvmlReturn_t result);

nvmlReturn_t nvmlDeviceGetCount_v2(unsigned int *deviceCount);
nvmlReturn_t nvmlDeviceGetHandleByIndex_v2(unsigned int index, nvmlDevice_t *device);
nvmlReturn_t nvmlDeviceGetName(nvmlDevice_t device, char *name, unsigned int length);
nvmlReturn_t nvmlDeviceGetTemperature(nvmlDevice_t device,
nvmlTemperatureSensors_t sensorType,
unsigned int *temp);
nvmlReturn_t nvmlDeviceGetUtilizationRates(nvmlDevice_t device,
nvmlUtilization_t *utilization);
nvmlReturn_t nvmlDeviceGetMemoryInfo(nvmlDevice_t device, nvmlMemory_t *memory);
nvmlReturn_t nvmlDeviceGetPowerUsage(nvmlDevice_t device, unsigned int *power);
nvmlReturn_t nvmlDeviceGetEnforcedPowerLimit(nvmlDevice_t device, unsigned int *limit);
nvmlReturn_t nvmlDeviceSetPersistenceMode(nvmlDevice_t device, nvmlEnableState_t mode);

nvmlReturn_t nvmlDeviceGetNumFans(nvmlDevice_t device, unsigned int *numFans);
nvmlReturn_t nvmlDeviceGetFanSpeed_v2(nvmlDevice_t device, unsigned int fan,
unsigned int *speed);
nvmlReturn_t nvmlDeviceSetFanSpeed_v2(nvmlDevice_t device, unsigned int fan,
unsigned int speed);
nvmlReturn_t nvmlDeviceSetDefaultFanSpeed_v2(nvmlDevice_t device, unsigned int fan);
nvmlReturn_t nvmlDeviceSetFanControlPolicy(nvmlDevice_t device, unsigned int fan,
nvmlFanControlPolicy_t policy);

/* nvml.h maps the unversioned names onto the current ABI the same way. */
#define nvmlInit nvmlInit_v2
#define nvmlDeviceGetCount nvmlDeviceGetCount_v2
#define nvmlDeviceGetHandleByIndex nvmlDeviceGetHandleByIndex_v2

#ifdef __cplusplus
}
#endif

#endif /* NVFD_NVML_API_H */
12 changes: 12 additions & 0 deletions scripts/find-nvml.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh

find_nvml_libdir() {
if [ -n "${NVML_LIBDIR:-}" ]; then
printf '%s\n' "$NVML_LIBDIR"
return
fi

"${LDCONFIG:-/sbin/ldconfig}" -p 2>/dev/null |
awk '/libnvidia-ml\.so\.1 \(libc6,/{print $NF; exit}' |
xargs -r dirname
}
43 changes: 30 additions & 13 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,49 @@ else
fi

echo "Detected OS: $OS"

SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck disable=SC1091
. "$SCRIPT_DIR/scripts/find-nvml.sh"

# NVML comes from the NVIDIA driver, never from a CUDA toolkit package. Find
# the driver library before touching anything, and pass that same directory to
# Make. NVML_LIBDIR remains an escape hatch for nonstandard driver layouts.
LDCONFIG=${LDCONFIG:-/sbin/ldconfig}
NVML_LIBDIR=$(find_nvml_libdir)
if [ -z "$NVML_LIBDIR" ]; then
echo "ERROR: libnvidia-ml.so.1 not found in the dynamic linker cache." >&2
echo " Install the NVIDIA driver (R520 or newer), or set NVML_LIBDIR." >&2
exit 1
fi
echo "Using NVML from $NVML_LIBDIR/libnvidia-ml.so.1"

echo "Installing dependencies..."

case "$OS" in
"Ubuntu"|"Ubuntu "*|"Debian GNU/Linux"|"Debian")
apt-get update
apt-get install -y build-essential libjansson-dev libncursesw5-dev nvidia-cuda-toolkit
apt-get install -y build-essential libjansson-dev libncursesw5-dev
;;
"Rocky Linux"|"CentOS Linux"|"Red Hat Enterprise Linux"|"Fedora"|"Fedora Linux")
dnf install -y gcc make jansson-devel ncurses-devel
;;
*)
echo "Warning: Unsupported OS ($OS). Ensure gcc, make, libjansson-dev, libncurses-dev, and NVML headers are installed."
echo "Warning: Unsupported OS ($OS). Ensure gcc, make, libjansson-dev and libncurses-dev are installed."
;;
esac

echo "Building NVFD..."
cd "$SCRIPT_DIR"
make clean && make NVML_LIBDIR="$NVML_LIBDIR" LDCONFIG="$LDCONFIG"

# Prove the fresh binary can initialise NVML on this host before anything on
# the system is changed. This also runs the v1.x config migration. If NVML
# cannot initialise here the daemon would not work either, and the running
# service (if any) is left untouched.
echo "Checking NVML and config migration..."
"$SCRIPT_DIR/build/nvfd" list

# Stop old service if running
if systemctl is-active --quiet infinirc-gpu-fan-control.service 2>/dev/null; then
echo "Stopping old v1 service..."
Expand All @@ -91,20 +119,9 @@ if systemctl is-active --quiet nvfd.service 2>/dev/null; then
systemctl stop nvfd.service
fi

# Determine script directory (where the repo is)
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"

echo "Building NVFD..."
cd "$SCRIPT_DIR"
make clean && make

echo "Installing..."
make install

# Run config migration
echo "Checking for config migration..."
/usr/local/bin/nvfd list >/dev/null 2>&1 || true

# Remove old alias if present
if grep -q 'alias igfc=' /etc/bash.bashrc 2>/dev/null; then
sed -i '/alias igfc=/d' /etc/bash.bashrc
Expand Down
13 changes: 8 additions & 5 deletions src/fan.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,16 @@ int fan_reset_to_auto(unsigned int gpu_index) {
}
}

/* Restore automatic fan policy if API is available */
#ifdef NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW
/* Also restore the driver's temperature-driven policy explicitly. */
for (int i = 0; i < num_fans; i++) {
nvmlDeviceSetFanControlPolicy(device, (unsigned int)i,
NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW);
nvmlReturn_t r = nvmlDeviceSetFanControlPolicy(device, (unsigned int)i,
NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW);
if (r != NVML_SUCCESS) {
fprintf(stderr, "Failed to restore auto fan policy for fan %d on GPU %u: %s\n",
i, gpu_index, nvmlErrorString(r));
failures++;
}
}
#endif

return failures;
}
Expand Down
32 changes: 32 additions & 0 deletions tests/nvml_stub.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Stub libnvidia-ml.so.1 for CI. Every entry point nvfd declares in
* include/nvml_api.h is defined here with that exact signature, so this file
* does two things: it lets `make` link on a runner with no driver, and it
* fails to compile if nvml_api.h drifts from the definitions below.
*
* Build: cc -shared -fPIC -Wl,-soname,libnvidia-ml.so.1 -Iinclude \
* -o <dir>/libnvidia-ml.so.1 tests/nvml_stub.c
* Then: make LDFLAGS=-L<dir>
*/
#include <stddef.h>
#include "nvml_api.h"

nvmlReturn_t nvmlInit_v2(void) { return NVML_SUCCESS; }
nvmlReturn_t nvmlShutdown(void) { return NVML_SUCCESS; }
const char *nvmlErrorString(nvmlReturn_t result) { (void)result; return "stub"; }

nvmlReturn_t nvmlDeviceGetCount_v2(unsigned int *deviceCount) { *deviceCount = 0; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceGetHandleByIndex_v2(unsigned int index, nvmlDevice_t *device) { (void)index; *device = NULL; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceGetName(nvmlDevice_t device, char *name, unsigned int length) { (void)device; if (length) name[0] = '\0'; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceGetTemperature(nvmlDevice_t device, nvmlTemperatureSensors_t sensorType, unsigned int *temp) { (void)device; (void)sensorType; *temp = 0; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceGetUtilizationRates(nvmlDevice_t device, nvmlUtilization_t *utilization) { (void)device; utilization->gpu = utilization->memory = 0; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceGetMemoryInfo(nvmlDevice_t device, nvmlMemory_t *memory) { (void)device; memory->total = memory->free = memory->used = 0; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceGetPowerUsage(nvmlDevice_t device, unsigned int *power) { (void)device; *power = 0; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceGetEnforcedPowerLimit(nvmlDevice_t device, unsigned int *limit) { (void)device; *limit = 0; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceSetPersistenceMode(nvmlDevice_t device, nvmlEnableState_t mode) { (void)device; (void)mode; return NVML_SUCCESS; }

nvmlReturn_t nvmlDeviceGetNumFans(nvmlDevice_t device, unsigned int *numFans) { (void)device; *numFans = 0; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceGetFanSpeed_v2(nvmlDevice_t device, unsigned int fan, unsigned int *speed) { (void)device; (void)fan; *speed = 0; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceSetFanSpeed_v2(nvmlDevice_t device, unsigned int fan, unsigned int speed) { (void)device; (void)fan; (void)speed; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceSetDefaultFanSpeed_v2(nvmlDevice_t device, unsigned int fan) { (void)device; (void)fan; return NVML_SUCCESS; }
nvmlReturn_t nvmlDeviceSetFanControlPolicy(nvmlDevice_t device, unsigned int fan, nvmlFanControlPolicy_t policy) { (void)device; (void)fan; (void)policy; return NVML_SUCCESS; }
20 changes: 20 additions & 0 deletions tests/test_find_nvml.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/sh
set -eu

repo_dir=$(cd -- "$(dirname "$0")/.." && pwd)
# shellcheck disable=SC1091
. "$repo_dir/scripts/find-nvml.sh"

tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT

cat >"$tmp_dir/ldconfig" <<'EOF'
#!/bin/sh
cat <<'CACHE'
libnvidia-ml.so.1 (libc6,x86-64) => /driver/lib/libnvidia-ml.so.1
CACHE
EOF
chmod +x "$tmp_dir/ldconfig"

[ "$(NVML_LIBDIR=/vendor/lib find_nvml_libdir)" = "/vendor/lib" ]
[ "$(LDCONFIG="$tmp_dir/ldconfig" NVML_LIBDIR='' find_nvml_libdir)" = "/driver/lib" ]
Loading
Loading