Skip to content

Instantly share code, notes, and snippets.

@darkylein
Last active March 20, 2025 22:30
Show Gist options
  • Select an option

  • Save darkylein/6e7edbcf2da72a6ef979ecac03d62be0 to your computer and use it in GitHub Desktop.

Select an option

Save darkylein/6e7edbcf2da72a6ef979ecac03d62be0 to your computer and use it in GitHub Desktop.
Nix lact module
{
config,
lib,
pkgs,
...
}:
let
# TODO: Add missing options.
# TODO: Add descriptions for every option.
# TODO: Add merging/concatenating of options with extraConfig or remove extraConfig entirely.
# TODO: Make sure settings submodule is not shown when rendered in documentation. -> Use
# `visible = "shallow"` in options?
cfg = config.myNixos.services.lact;
settingsFormat = pkgs.formats.yaml { };
gpusType = lib.mkOption {
default = null;
type = lib.types.nullOr (
lib.types.attrsOf (
lib.types.submodule {
options = {
fan_control_enabled = lib.mkOption {
default = false;
example = true;
type = lib.types.bool;
};
fan_control_settings = lib.mkOption {
default = null;
type = lib.types.nullOr (
lib.types.submodule {
options = {
mode = lib.mkOption {
default = "curve";
example = "static";
type = lib.types.enum [
"curve"
"static"
];
};
static_speed = lib.mkOption {
default = 0.5;
example = 1.0;
description = "number between 0 and 1";
type = lib.types.numbers.between 0 1;
};
temperature_key = lib.mkOption {
default = "edge";
example = "junction";
type = lib.types.enum [
"edge"
"junction"
];
};
interval_ms = lib.mkOption {
default = 500;
example = 1000;
description = "integer greater 0";
type = lib.types.ints.positive;
};
curve = lib.mkOption {
# Can the key be type checked?
default = {
"40" = 0.2;
"50" = 0.35;
"60" = 0.5;
"70" = 0.75;
"80" = 1.0;
};
example = {
"40" = 0.2;
"50" = 0.35;
"60" = 0.5;
"70" = 0.75;
"80" = 1.0;
};
type = lib.types.attrsOf lib.types.numbers.nonnegative;
};
spindown_delay_ms = lib.mkOption {
default = 0;
example = 5000;
type = lib.types.ints.between 0 29990;
};
change_threshold = lib.mkOption {
default = 0;
example = 3;
type = lib.types.ints.between 0 9;
};
};
}
);
};
power_cap = lib.mkOption {
default = null;
example = 320.0;
type = lib.types.nullOr lib.types.ints.positive;
};
performance_level = lib.mkOption {
default = "auto";
example = "manual";
type = lib.types.nullOr (
lib.types.enum [
"auto"
"low"
"high"
"manual"
]
);
};
power_profile_mode_index = lib.mkOption {
# TODO: Add check if performance_level is set to manual.
default = null;
example = 1;
type = lib.types.nullOr lib.types.ints.unsigned; # How many indices are there?
};
min_core_clock = lib.mkOption {
default = null;
example = 300;
type = lib.types.nullOr lib.types.ints.unsigned;
};
min_memory_clock = lib.mkOption {
default = null;
example = 500;
type = lib.types.nullOr lib.types.ints.unsigned;
};
min_voltage = lib.mkOption {
default = null;
example = 900;
type = lib.types.nullOr lib.types.ints.unsigned;
};
max_core_clock = lib.mkOption {
default = null;
example = 1630;
type = lib.types.nullOr lib.types.ints.positive;
};
max_memory_clock = lib.mkOption {
default = null;
example = 800;
type = lib.types.nullOr lib.types.ints.positive;
};
max_voltage = lib.mkOption {
default = null;
example = 1200;
type = lib.types.nullOr lib.types.ints.positive;
};
voltage_offset = lib.mkOption {
default = null;
example = -50;
type = lib.types.nullOr lib.types.int;
};
};
}
)
);
};
in
{
options.myNixos.services.lact = {
enable = lib.mkEnableOption "LACT Linux GPU Control Application";
extraConfig = lib.mkOption {
default = "";
example = "tcp_listen_address: 127.0.0.1:12853";
type = lib.types.lines;
};
package = lib.mkPackageOption pkgs "lact" { };
settings = lib.mkOption {
default = { };
type = lib.types.submodule {
options = {
daemon = {
log_level = lib.mkOption {
default = "info";
type = lib.types.enum [
"error"
"warn"
"info"
"debug"
"trace"
];
};
admin_groups = lib.mkOption {
default = [
"wheel"
"sudo"
];
example = [ "sudo" ];
type = lib.types.listOf lib.types.str;
};
disable_clocks_cleanup = lib.mkOption {
default = false;
example = true;
type = lib.types.bool;
};
};
apply_settings_timer = lib.mkOption {
default = 5;
example = 10;
type = lib.types.ints.unsigned;
};
gpus = gpusType;
profiles = lib.mkOption {
default = null;
type = lib.types.nullOr (
lib.types.attrsOf (
lib.types.submodule {
options = {
gpus = gpusType;
# NOTE: Rules is not supported yet by current lact version in nixpkgs.
rules = lib.mkOption {
default = null;
type = lib.types.nullOr (
lib.types.submodule {
options = {
type = lib.mkOption {
example = "process";
type = lib.types.enum [
"gamemode"
"process"
];
};
rule = lib.mkOption {
default = null;
type = lib.types.nullOr (
lib.types.submodule {
options = {
name = lib.mkOption {
example = "vkcube";
type = lib.types.str;
};
args = lib.mkOption {
default = null;
example = "--my-arg";
type = lib.types.nullOr lib.types.str;
};
};
}
);
};
};
}
);
};
};
}
)
);
};
current_profile = lib.mkOption {
default = null;
example = "vkcube";
type = lib.types.nullOr lib.types.str;
};
auto_switch_profiles = lib.mkOption {
default = null;
example = true;
type = lib.types.nullOr lib.types.bool;
};
};
};
};
};
config = lib.mkIf cfg.enable {
environment = {
etc."lact/config.yaml" = lib.mkIf (cfg.settings != { }) {
# NOTE: Feel free to let me know or open a pull request if there is a cleaner way to
# concatenate the config with extraConfig.
source =
let
# Recursively remove attributes with value null.
filteredConfig = lib.filterAttrsRecursive (n: v: v != null) cfg;
initialConfig = settingsFormat.generate "etc-lact-config-initial-config.yaml" filteredConfig.settings;
# Workaround for lact’s strict integer key requirement.
postProcessedYaml =
pkgs.runCommand "etc-lact-config-processed.yaml"
{
nativeBuildInputs = with pkgs; [
python3
python3Packages.pyyaml
];
}
''
cp ${initialConfig} initial-config.yaml
python3 <<EOF
import yaml
def conv_keys_into_int(some_dict):
return {int(k): v for k, v in some_dict.items()}
def process_gpus(gpus):
for gpu_name, gpu_data in gpus.items():
if (
"fan_control_settings" in gpu_data
and "curve" in gpu_data["fan_control_settings"]
):
gpus[gpu_name]["fan_control_settings"]["curve"] = conv_keys_into_int(
gpu_data["fan_control_settings"]["curve"]
)
return gpus
def process_profiles(profiles):
for profile_name, profile_data in profiles.items():
if "gpus" in profile_data:
profiles[profile_name]["gpus"] = process_gpus(profile_data["gpus"])
return profiles
def main():
with open("initial-config.yaml") as f:
data = yaml.safe_load(f) or {}
if "gpus" in data:
data["gpus"] = process_gpus(data["gpus"])
if "profiles" in data:
data["profiles"] = process_profiles(data["profiles"])
with open("$out", "w") as f:
yaml.dump(data, f, default_flow_style=False)
if __name__ == "__main__":
main()
EOF
'';
in
pkgs.writeText "etc-lact-config.yaml" (
lib.concatStrings [
(builtins.readFile postProcessedYaml)
"\n"
cfg.extraConfig
]
);
};
systemPackages = [ cfg.package ];
};
systemd.services.lactd = {
after = [ "multi-user.target" ];
description = "LACT Linux GPU Control Daemon";
path = with pkgs; [ lact ];
serviceConfig = {
ExecStart = "${pkgs.lact}/bin/lact daemon";
Nice = -10;
};
wantedBy = [ "multi-user.target" ];
};
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment