Browse Source

Add an option for using a different governor for integrated GPUs

This commit adds two new configuration options: igpu_desiredgov and
igpu_power_threshold which allow for a different CPU governor when the
Intel integrated GPU is under load.  This currently only applies to
Intel integrated GPUs and not AMD APUs because it uses the Intel RAPL
infrastructure for getting power information.  If on a platform that
without an Intel integrated GPU or where the kernel does not support
RAPL, the new options will be ignored and it will fall back to the old
behavior.

One of the core principals of gamemoded to date has been that, when
playing a game, we want to use the "performance" CPU governor to
increase CPU performance and prevent CPU-limiting.  However, when the
integrated GPU is under load, this can be counter-productive because the
CPU and GPU share a thermal and power budget.  By throwing the CPU
governor to "performance" game mode currently makes the CPU frequency
management far too aggressive and it burns more power than needed.  With
a discrete GPU, this is fine because the worst that happens is a bit
more fan noise.  With an integrated GPU, however, the additional power
being burned by the CPU is power not available to the GPU and this can
cause the GPU to clock down and lead to significantly worse performance.

By using the "powersave" governor instead of the "performance" governor
while the integrated GPU is under load, we can save power on the CPU
side which lets the GPU clock up higher.  On my Razer Blade Stealth 13
with an i7-1065G7, this improves the performance of "Shadow of the Tomb
Raider" by around 25-30% according to its internal benchmark mode.
Jason Ekstrand 5 years ago
parent
commit
688373a260
4 changed files with 182 additions and 2 deletions
  1. 61 0
      daemon/gamemode-config.c
  2. 2 0
      daemon/gamemode-config.h
  3. 109 1
      daemon/gamemode-context.c
  4. 10 1
      example/gamemode.ini

+ 61 - 0
daemon/gamemode-config.c

@@ -39,6 +39,7 @@ POSSIBILITY OF SUCH DAMAGE.
 #include "ini.h"
 
 #include <dirent.h>
+#include <math.h>
 #include <pthread.h>
 #include <pwd.h>
 #include <sys/inotify.h>
@@ -50,6 +51,8 @@ POSSIBILITY OF SUCH DAMAGE.
 /* Default value for the reaper frequency */
 #define DEFAULT_REAPER_FREQ 5
 
+#define DEFAULT_IGPU_POWER_THRESHOLD 0.3f
+
 /* Helper macro for defining the config variable getter */
 #define DEFINE_CONFIG_GET(name)                                                                    \
 	long config_get_##name(GameModeConfig *self)                                                   \
@@ -82,6 +85,9 @@ struct GameModeConfig {
 		char defaultgov[CONFIG_VALUE_MAX];
 		char desiredgov[CONFIG_VALUE_MAX];
 
+		char igpu_desiredgov[CONFIG_VALUE_MAX];
+		float igpu_power_threshold;
+
 		char softrealtime[CONFIG_VALUE_MAX];
 		long renice;
 
@@ -179,6 +185,27 @@ __attribute__((unused)) static bool get_long_value_hex(const char *value_name, c
 	return true;
 }
 
+/*
+ * Get a long value from a string
+ */
+static bool get_float_value(const char *value_name, const char *value, float *output)
+{
+	char *end = NULL;
+	float config_value = strtof(value, &end);
+
+	if (errno == ERANGE) {
+		LOG_ERROR("Config: %s overflowed, given [%s]\n", value_name, value);
+		return false;
+	} else if (!(*value != '\0' && end && *end == '\0')) {
+		LOG_ERROR("Config: %s was invalid, given [%s]\n", value_name, value);
+		return false;
+	} else {
+		*output = config_value;
+	}
+
+	return true;
+}
+
 /**
  * Simple strstr scheck
  * Could be expanded for wildcard or regex
@@ -230,6 +257,10 @@ static int inih_handler(void *user, const char *section, const char *name, const
 			valid = get_string_value(value, self->values.defaultgov);
 		} else if (strcmp(name, "desiredgov") == 0) {
 			valid = get_string_value(value, self->values.desiredgov);
+		} else if (strcmp(name, "igpu_desiredgov") == 0) {
+			valid = get_string_value(value, self->values.igpu_desiredgov);
+		} else if (strcmp(name, "igpu_power_threshold") == 0) {
+			valid = get_float_value(name, value, &self->values.igpu_power_threshold);
 		} else if (strcmp(name, "softrealtime") == 0) {
 			valid = get_string_value(value, self->values.softrealtime);
 		} else if (strcmp(name, "renice") == 0) {
@@ -329,6 +360,7 @@ static void load_config_files(GameModeConfig *self)
 	memset(&self->values, 0, sizeof(self->values));
 
 	/* Set some non-zero defaults */
+	self->values.igpu_power_threshold = DEFAULT_IGPU_POWER_THRESHOLD;
 	self->values.inhibit_screensaver = 1; /* Defaults to on */
 	self->values.reaper_frequency = DEFAULT_REAPER_FREQ;
 	self->values.gpu_device = -1; /* 0 is a valid device ID so use -1 to indicate no value */
@@ -641,6 +673,35 @@ void config_get_desired_governor(GameModeConfig *self, char governor[CONFIG_VALU
 	memcpy_locked_config(self, governor, self->values.desiredgov, sizeof(self->values.desiredgov));
 }
 
+/*
+ * Get the chosen iGPU desired governor
+ */
+void config_get_igpu_desired_governor(GameModeConfig *self, char governor[CONFIG_VALUE_MAX])
+{
+	memcpy_locked_config(self,
+	                     governor,
+	                     self->values.igpu_desiredgov,
+	                     sizeof(self->values.igpu_desiredgov));
+}
+
+/*
+ * Get the chosen iGPU power threshold
+ */
+float config_get_igpu_power_threshold(GameModeConfig *self)
+{
+	float value = 0;
+	memcpy_locked_config(self, &value, &self->values.igpu_power_threshold, sizeof(float));
+	/* Validate the threshold value */
+	if (isnan(value) || value < 0) {
+		LOG_ONCE(ERROR,
+		         "Configured iGPU power threshold value '%f' is invalid, ignoring iGPU default "
+		         "governor.\n",
+		         value);
+		value = FP_INFINITE;
+	}
+	return value;
+}
+
 /*
  * Get the chosen soft realtime behavior
  */

+ 2 - 0
daemon/gamemode-config.h

@@ -102,6 +102,8 @@ bool config_get_inhibit_screensaver(GameModeConfig *self);
 long config_get_script_timeout(GameModeConfig *self);
 void config_get_default_governor(GameModeConfig *self, char governor[CONFIG_VALUE_MAX]);
 void config_get_desired_governor(GameModeConfig *self, char governor[CONFIG_VALUE_MAX]);
+void config_get_igpu_desired_governor(GameModeConfig *self, char governor[CONFIG_VALUE_MAX]);
+float config_get_igpu_power_threshold(GameModeConfig *self);
 void config_get_soft_realtime(GameModeConfig *self, char softrealtime[CONFIG_VALUE_MAX]);
 long config_get_renice_value(GameModeConfig *self);
 long config_get_ioprio_value(GameModeConfig *self);

+ 109 - 1
daemon/gamemode-context.c

@@ -35,6 +35,7 @@ POSSIBILITY OF SUCH DAMAGE.
 #include "common-governors.h"
 #include "common-helpers.h"
 #include "common-logging.h"
+#include "common-power.h"
 
 #include "gamemode.h"
 #include "gamemode-config.h"
@@ -66,6 +67,7 @@ struct GameModeClient {
 enum GameModeGovernor {
 	GAME_MODE_GOVERNOR_DEFAULT,
 	GAME_MODE_GOVERNOR_DESIRED,
+	GAME_MODE_GOVERNOR_IGPU_DESIRED,
 };
 
 struct GameModeContext {
@@ -82,6 +84,10 @@ struct GameModeContext {
 	struct GameModeGPUInfo *stored_gpu; /**<Stored GPU info for the current GPU */
 	struct GameModeGPUInfo *target_gpu; /**<Target GPU info for the current GPU */
 
+	bool igpu_optimization_enabled;
+	uint32_t last_cpu_energy_uj;
+	uint32_t last_igpu_energy_uj;
+
 	/* Reaper control */
 	struct {
 		pthread_t thread;
@@ -219,6 +225,11 @@ static int game_mode_set_governor(GameModeContext *self, enum GameModeGovernor g
 		gov_str = gov_config_str[0] != '\0' ? gov_config_str : "performance";
 		break;
 
+	case GAME_MODE_GOVERNOR_IGPU_DESIRED:
+		config_get_igpu_desired_governor(self->config, gov_config_str);
+		gov_str = gov_config_str[0] != '\0' ? gov_config_str : "powersave";
+		break;
+
 	default:
 		assert(!"Invalid governor requested");
 	}
@@ -240,6 +251,93 @@ static int game_mode_set_governor(GameModeContext *self, enum GameModeGovernor g
 	return 0;
 }
 
+static void game_mode_enable_igpu_optimization(GameModeContext *self)
+{
+	float threshold = config_get_igpu_power_threshold(self->config);
+
+	/* There's no way the GPU is using 10000x the power.  This lets us
+	 * short-circuit if the config file specifies an invalid threshold
+	 * and we want to disable the iGPU heuristic.
+	 */
+	if (threshold < 10000 && get_cpu_energy_uj(&self->last_cpu_energy_uj) &&
+	    get_igpu_energy_uj(&self->last_igpu_energy_uj)) {
+		LOG_MSG(
+		    "Successfully queried power data for the CPU and iGPU. "
+		    "Enabling the integrated GPU optimization");
+		self->igpu_optimization_enabled = true;
+	}
+}
+
+static void game_mode_disable_igpu_optimization(GameModeContext *self)
+{
+	self->igpu_optimization_enabled = false;
+}
+
+static void game_mode_check_igpu_energy(GameModeContext *self)
+{
+	pthread_rwlock_wrlock(&self->rwlock);
+
+	/* We only care if we're not in the default governor */
+	if (self->current_govenor == GAME_MODE_GOVERNOR_DEFAULT)
+		goto unlock;
+
+	if (!self->igpu_optimization_enabled)
+		goto unlock;
+
+	uint32_t cpu_energy_uj, igpu_energy_uj;
+	if (!get_cpu_energy_uj(&cpu_energy_uj) || !get_igpu_energy_uj(&igpu_energy_uj)) {
+		/* We've already succeeded at getting power information once so
+		 * failing here is possible but very unexpected. */
+		self->igpu_optimization_enabled = false;
+		LOG_ERROR("Failed to get CPU and iGPU power data\n");
+		goto unlock;
+	}
+
+	/* The values we query from RAPL are in units of microjoules of energy
+	 * used since boot or since the last time the counter rolled over.  You
+	 * can get average power over some time window T by sampling before and
+	 * after and doing the following calculation
+	 *
+	 *     power_uw = (energy_uj_after - energy_uj_before) / seconds
+	 *
+	 * To get the power in Watts (rather than microwatts), you can simply
+	 * divide by 1000000.
+	 *
+	 * Because we're only concerned with the ratio between the GPU and CPU
+	 * power, we never bother dividing by 1000000 the length of time of the
+	 * sampling window because that would just algebraically cancel out.
+	 * Instead, we divide the GPU energy used in the window (difference of
+	 * before and after) by the CPU energy used. It nicely provides the
+	 * ratio of the averages and there are no instantaneous sampling
+	 * problems.
+	 *
+	 * Overflow is possible here.  However, that would simply mean that
+	 * the HW counter has overflowed and us wrapping around is probably
+	 * the right thing to do.  Wrapping at 32 bits is exactly what the
+	 * Linux kernel's turbostat utility does so it's probably right.
+	 */
+	uint32_t cpu_energy_delta_uj = cpu_energy_uj - self->last_cpu_energy_uj;
+	uint32_t igpu_energy_delta_uj = igpu_energy_uj - self->last_igpu_energy_uj;
+	self->last_cpu_energy_uj = cpu_energy_uj;
+	self->last_igpu_energy_uj = igpu_energy_uj;
+
+	if (cpu_energy_delta_uj == 0) {
+		LOG_ERROR("CPU reported no energy used\n");
+		goto unlock;
+	}
+
+	float threshold = config_get_igpu_power_threshold(self->config);
+	double ratio = (double)igpu_energy_delta_uj / (double)cpu_energy_delta_uj;
+	if (ratio > threshold) {
+		game_mode_set_governor(self, GAME_MODE_GOVERNOR_IGPU_DESIRED);
+	} else {
+		game_mode_set_governor(self, GAME_MODE_GOVERNOR_DESIRED);
+	}
+
+unlock:
+	pthread_rwlock_unlock(&self->rwlock);
+}
+
 /**
  * Pivot into game mode.
  *
@@ -251,7 +349,12 @@ static void game_mode_context_enter(GameModeContext *self)
 	LOG_MSG("Entering Game Mode...\n");
 	sd_notifyf(0, "STATUS=%sGameMode is now active.%s\n", "\x1B[1;32m", "\x1B[0m");
 
-	game_mode_set_governor(self, GAME_MODE_GOVERNOR_DESIRED);
+	if (game_mode_set_governor(self, GAME_MODE_GOVERNOR_DESIRED) == 0) {
+		/* We just switched to a non-default governor.  Enable the iGPU
+		 * optimization.
+		 */
+		game_mode_enable_igpu_optimization(self);
+	}
 
 	/* Inhibit the screensaver */
 	if (config_get_inhibit_screensaver(self->config))
@@ -290,6 +393,8 @@ static void game_mode_context_leave(GameModeContext *self)
 
 	game_mode_set_governor(self, GAME_MODE_GOVERNOR_DEFAULT);
 
+	game_mode_disable_igpu_optimization(self);
+
 	char scripts[CONFIG_LIST_MAX][CONFIG_VALUE_MAX];
 	memset(scripts, 0, sizeof(scripts));
 	config_get_gamemode_end_scripts(self->config, scripts);
@@ -822,6 +927,9 @@ static void *game_mode_context_reaper(void *userdata)
 			return NULL;
 		}
 
+		/* Check on the CPU/iGPU energy balance */
+		game_mode_check_igpu_energy(self);
+
 		/* Expire remaining entries */
 		game_mode_context_auto_expire(self);
 

+ 10 - 1
example/gamemode.ini

@@ -1,5 +1,5 @@
 [general]
-; The reaper thread will check every 5 seconds for exited clients and for config file changes
+; The reaper thread will check every 5 seconds for exited clients, for config file changes, and for the CPU/iGPU power balance
 reaper_freq=5
 
 ; The desired governor is used when entering GameMode instead of "performance"
@@ -7,6 +7,15 @@ desiredgov=performance
 ; The default governer is used when leaving GameMode instead of restoring the original value
 defaultgov=powersave
 
+; The iGPU desired governor is used when the integrated GPU is under heavy load
+igpu_desiredgov=powersave
+; Threshold to use to decide when the integrated GPU is under heavy load.
+; This is a ratio of iGPU Watts / CPU Watts which is used to determine when the
+; integraged GPU is under heavy enough load to justify switching to
+; igpu_desiredgov.  Set this to -1 to disable all iGPU checking and always
+; use desiredgov for games.
+igpu_power_threshold=0.3
+
 ; GameMode can change the scheduler policy to SCHED_ISO on kernels which support it (currently
 ; not supported by upstream kernels). Can be set to "auto", "on" or "off". "auto" will enable
 ; with 4 or more CPU cores. "on" will always enable. Defaults to "off".