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.

Revisions

  1. darkylein revised this gist Mar 20, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion default.nix
    Original file line number Diff line number Diff line change
    @@ -372,7 +372,7 @@ in

    systemd.services.lactd = {
    after = [ "multi-user.target" ];
    description = "LACT Linux GPU Control Application";
    description = "LACT Linux GPU Control Daemon";
    path = with pkgs; [ lact ];
    serviceConfig = {
    ExecStart = "${pkgs.lact}/bin/lact daemon";
  2. darkylein created this gist Mar 20, 2025.
    384 changes: 384 additions & 0 deletions default.nix
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,384 @@
    {
    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 Application";
    path = with pkgs; [ lact ];
    serviceConfig = {
    ExecStart = "${pkgs.lact}/bin/lact daemon";
    Nice = -10;
    };
    wantedBy = [ "multi-user.target" ];
    };
    };
    }