Last active
March 20, 2025 22:30
-
-
Save darkylein/6e7edbcf2da72a6ef979ecac03d62be0 to your computer and use it in GitHub Desktop.
Nix lact module
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| 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