#!/bin/sh # # This is an utility script to manage Intel GPU frequencies. # It can be used for debugging performance problems or trying to obtain a stable # frequency while benchmarking. # # Note the Intel i915 GPU driver allows to change the minimum, maximum and boost # frequencies in steps of 50 MHz via: # # /sys/class/drm/card/ # # Where is the DRM card index and one of the following: # # - gt_max_freq_mhz (enforced maximum freq) # - gt_min_freq_mhz (enforced minimum freq) # - gt_boost_freq_mhz (enforced boost freq) # # The hardware capabilities can be accessed via: # # - gt_RP0_freq_mhz (supported maximum freq) # - gt_RPn_freq_mhz (supported minimum freq) # - gt_RP1_freq_mhz (most efficient freq) # # The current frequency can be read from: # - gt_act_freq_mhz (the actual GPU freq) # - gt_cur_freq_mhz (the last requested freq) # # Also note that in addition to GPU management, the script offers the # possibility to adjust CPU operating frequencies. However, this is currently # limited to just setting the maximum scaling frequency as percentage of the # maximum frequency allowed by the hardware. # # Copyright (C) 2022 Collabora Ltd. # Author: Cristian Ciocaltea # # SPDX-License-Identifier: MIT # # # Constants # # GPU DRM_FREQ_SYSFS_PATTERN="/sys/class/drm/card%d/gt_%s_freq_mhz" ENF_FREQ_INFO="max min boost" CAP_FREQ_INFO="RP0 RPn RP1" ACT_FREQ_INFO="act cur" THROTT_DETECT_SLEEP_SEC=2 THROTT_DETECT_PID_FILE_PATH=/tmp/thrott-detect.pid # CPU CPU_SYSFS_PREFIX=/sys/devices/system/cpu CPU_PSTATE_SYSFS_PATTERN="${CPU_SYSFS_PREFIX}/intel_pstate/%s" CPU_FREQ_SYSFS_PATTERN="${CPU_SYSFS_PREFIX}/cpu%s/cpufreq/%s_freq" CAP_CPU_FREQ_INFO="cpuinfo_max cpuinfo_min" ENF_CPU_FREQ_INFO="scaling_max scaling_min" ACT_CPU_FREQ_INFO="scaling_cur" # # Global variables. # unset INTEL_DRM_CARD_INDEX unset GET_ACT_FREQ GET_ENF_FREQ GET_CAP_FREQ unset SET_MIN_FREQ SET_MAX_FREQ unset MONITOR_FREQ unset CPU_SET_MAX_FREQ unset DETECT_THROTT unset DRY_RUN # # Simple printf based stderr logger. # log() { local msg_type=$1 shift printf "%s: %s: " "${msg_type}" "${0##*/}" >&2 printf "$@" >&2 printf "\n" >&2 } # # Helper to print sysfs path for the given card index and freq info. # # arg1: Frequency info sysfs name, one of *_FREQ_INFO constants above # arg2: Video card index, defaults to INTEL_DRM_CARD_INDEX # print_freq_sysfs_path() { printf ${DRM_FREQ_SYSFS_PATTERN} "${2:-${INTEL_DRM_CARD_INDEX}}" "$1" } # # Helper to set INTEL_DRM_CARD_INDEX for the first identified Intel video card. # identify_intel_gpu() { local i=0 vendor path while [ ${i} -lt 16 ]; do [ -c "/dev/dri/card$i" ] || { i=$((i + 1)) continue } path=$(print_freq_sysfs_path "" ${i}) path=${path%/*}/device/vendor [ -r "${path}" ] && read vendor < "${path}" && \ [ "${vendor}" = "0x8086" ] && INTEL_DRM_CARD_INDEX=$i && return 0 i=$((i + 1)) done return 1 } # # Read the specified freq info from sysfs. # # arg1: Flag (y/n) to also enable printing the freq info. # arg2...: Frequency info sysfs name(s), see *_FREQ_INFO constants above # return: Global variable(s) FREQ_${arg} containing the requested information # read_freq_info() { local var val info path print=0 ret=0 [ "$1" = "y" ] && print=1 shift while [ $# -gt 0 ]; do info=$1 shift var=FREQ_${info} path=$(print_freq_sysfs_path "${info}") [ -r ${path} ] && read ${var} < ${path} || { log ERROR "Failed to read freq info from: %s" "${path}" ret=1 continue } [ -n "${var}" ] || { log ERROR "Got empty freq info from: %s" "${path}" ret=1 continue } [ ${print} -eq 1 ] && { eval val=\$${var} printf "%6s: %4s MHz\n" "${info}" "${val}" } done return ${ret} } # # Display requested info. # print_freq_info() { local req_freq [ -n "${GET_CAP_FREQ}" ] && { printf "* Hardware capabilities\n" read_freq_info y ${CAP_FREQ_INFO} printf "\n" } [ -n "${GET_ENF_FREQ}" ] && { printf "* Enforcements\n" read_freq_info y ${ENF_FREQ_INFO} printf "\n" } [ -n "${GET_ACT_FREQ}" ] && { printf "* Actual\n" read_freq_info y ${ACT_FREQ_INFO} printf "\n" } } # # Helper to print frequency value as requested by user via '-s, --set' option. # arg1: user requested freq value # compute_freq_set() { local val case "$1" in +) val=${FREQ_RP0} ;; -) val=${FREQ_RPn} ;; *%) val=$((${1%?} * ${FREQ_RP0} / 100)) # Adjust freq to comply with 50 MHz increments val=$((val / 50 * 50)) ;; *[!0-9]*) log ERROR "Cannot set freq to invalid value: %s" "$1" return 1 ;; "") log ERROR "Cannot set freq to unspecified value" return 1 ;; *) # Adjust freq to comply with 50 MHz increments val=$(($1 / 50 * 50)) ;; esac printf "%s" "${val}" } # # Helper for set_freq(). # set_freq_max() { log INFO "Setting GPU max freq to %s MHz" "${SET_MAX_FREQ}" read_freq_info n min || return $? [ ${SET_MAX_FREQ} -gt ${FREQ_RP0} ] && { log ERROR "Cannot set GPU max freq (%s) to be greater than hw max freq (%s)" \ "${SET_MAX_FREQ}" "${FREQ_RP0}" return 1 } [ ${SET_MAX_FREQ} -lt ${FREQ_RPn} ] && { log ERROR "Cannot set GPU max freq (%s) to be less than hw min freq (%s)" \ "${SET_MIN_FREQ}" "${FREQ_RPn}" return 1 } [ ${SET_MAX_FREQ} -lt ${FREQ_min} ] && { log ERROR "Cannot set GPU max freq (%s) to be less than min freq (%s)" \ "${SET_MAX_FREQ}" "${FREQ_min}" return 1 } [ -z "${DRY_RUN}" ] || return 0 printf "%s" ${SET_MAX_FREQ} | tee $(print_freq_sysfs_path max) \ $(print_freq_sysfs_path boost) > /dev/null [ $? -eq 0 ] || { log ERROR "Failed to set GPU max frequency" return 1 } } # # Helper for set_freq(). # set_freq_min() { log INFO "Setting GPU min freq to %s MHz" "${SET_MIN_FREQ}" read_freq_info n max || return $? [ ${SET_MIN_FREQ} -gt ${FREQ_max} ] && { log ERROR "Cannot set GPU min freq (%s) to be greater than max freq (%s)" \ "${SET_MIN_FREQ}" "${FREQ_max}" return 1 } [ ${SET_MIN_FREQ} -lt ${FREQ_RPn} ] && { log ERROR "Cannot set GPU min freq (%s) to be less than hw min freq (%s)" \ "${SET_MIN_FREQ}" "${FREQ_RPn}" return 1 } [ -z "${DRY_RUN}" ] || return 0 printf "%s" ${SET_MIN_FREQ} > $(print_freq_sysfs_path min) [ $? -eq 0 ] || { log ERROR "Failed to set GPU min frequency" return 1 } } # # Set min or max or both GPU frequencies to the user indicated values. # set_freq() { # Get hw max & min frequencies read_freq_info n RP0 RPn || return $? [ -z "${SET_MAX_FREQ}" ] || { SET_MAX_FREQ=$(compute_freq_set "${SET_MAX_FREQ}") [ -z "${SET_MAX_FREQ}" ] && return 1 } [ -z "${SET_MIN_FREQ}" ] || { SET_MIN_FREQ=$(compute_freq_set "${SET_MIN_FREQ}") [ -z "${SET_MIN_FREQ}" ] && return 1 } # # Ensure correct operation order, to avoid setting min freq # to a value which is larger than max freq. # # E.g.: # crt_min=crt_max=600; new_min=new_max=700 # > operation order: max=700; min=700 # # crt_min=crt_max=600; new_min=new_max=500 # > operation order: min=500; max=500 # if [ -n "${SET_MAX_FREQ}" ] && [ -n "${SET_MIN_FREQ}" ]; then [ ${SET_MAX_FREQ} -lt ${SET_MIN_FREQ} ] && { log ERROR "Cannot set GPU max freq to be less than min freq" return 1 } read_freq_info n min || return $? if [ ${SET_MAX_FREQ} -lt ${FREQ_min} ]; then set_freq_min || return $? set_freq_max else set_freq_max || return $? set_freq_min fi elif [ -n "${SET_MAX_FREQ}" ]; then set_freq_max elif [ -n "${SET_MIN_FREQ}" ]; then set_freq_min else log "Unexpected call to set_freq()" return 1 fi } # # Helper for detect_throttling(). # get_thrott_detect_pid() { [ -e ${THROTT_DETECT_PID_FILE_PATH} ] || return 0 local pid read pid < ${THROTT_DETECT_PID_FILE_PATH} || { log ERROR "Failed to read pid from: %s" "${THROTT_DETECT_PID_FILE_PATH}" return 1 } local proc_path=/proc/${pid:-invalid}/cmdline [ -r ${proc_path} ] && grep -qs "${0##*/}" ${proc_path} && { printf "%s" "${pid}" return 0 } # Remove orphaned PID file rm -rf ${THROTT_DETECT_PID_FILE_PATH} return 1 } # # Control detection and reporting of GPU throttling events. # arg1: start - run throttle detector in background # stop - stop throttle detector process, if any # status - verify if throttle detector is running # detect_throttling() { local pid pid=$(get_thrott_detect_pid) case "$1" in status) printf "Throttling detector is " [ -z "${pid}" ] && printf "not running\n" && return 0 printf "running (pid=%s)\n" ${pid} ;; stop) [ -z "${pid}" ] && return 0 log INFO "Stopping throttling detector (pid=%s)" "${pid}" kill ${pid}; sleep 1; kill -0 ${pid} 2>/dev/null && kill -9 ${pid} rm -rf ${THROTT_DETECT_PID_FILE_PATH} ;; start) [ -n "${pid}" ] && { log WARN "Throttling detector is already running (pid=%s)" ${pid} return 0 } ( read_freq_info n RPn || exit $? while true; do sleep ${THROTT_DETECT_SLEEP_SEC} read_freq_info n act min cur || exit $? # # The throttling seems to occur when act freq goes below min. # However, it's necessary to exclude the idle states, where # act freq normally reaches RPn and cur goes below min. # [ ${FREQ_act} -lt ${FREQ_min} ] && \ [ ${FREQ_act} -gt ${FREQ_RPn} ] && \ [ ${FREQ_cur} -ge ${FREQ_min} ] && \ printf "GPU throttling detected: act=%s min=%s cur=%s RPn=%s\n" \ ${FREQ_act} ${FREQ_min} ${FREQ_cur} ${FREQ_RPn} done ) & pid=$! log INFO "Started GPU throttling detector (pid=%s)" ${pid} printf "%s\n" ${pid} > ${THROTT_DETECT_PID_FILE_PATH} || \ log WARN "Failed to write throttle detector PID file" ;; esac } # # Retrieve the list of online CPUs. # get_online_cpus() { local path cpu_index printf "0" for path in $(grep 1 ${CPU_SYSFS_PREFIX}/cpu*/online); do cpu_index=${path##*/cpu} printf " %s" ${cpu_index%%/*} done } # # Helper to print sysfs path for the given CPU index and freq info. # # arg1: Frequency info sysfs name, one of *_CPU_FREQ_INFO constants above # arg2: CPU index # print_cpu_freq_sysfs_path() { printf ${CPU_FREQ_SYSFS_PATTERN} "$2" "$1" } # # Read the specified CPU freq info from sysfs. # # arg1: CPU index # arg2: Flag (y/n) to also enable printing the freq info. # arg3...: Frequency info sysfs name(s), see *_CPU_FREQ_INFO constants above # return: Global variable(s) CPU_FREQ_${arg} containing the requested information # read_cpu_freq_info() { local var val info path cpu_index print=0 ret=0 cpu_index=$1 [ "$2" = "y" ] && print=1 shift 2 while [ $# -gt 0 ]; do info=$1 shift var=CPU_FREQ_${info} path=$(print_cpu_freq_sysfs_path "${info}" ${cpu_index}) [ -r ${path} ] && read ${var} < ${path} || { log ERROR "Failed to read CPU freq info from: %s" "${path}" ret=1 continue } [ -n "${var}" ] || { log ERROR "Got empty CPU freq info from: %s" "${path}" ret=1 continue } [ ${print} -eq 1 ] && { eval val=\$${var} printf "%6s: %4s Hz\n" "${info}" "${val}" } done return ${ret} } # # Helper to print freq. value as requested by user via '--cpu-set-max' option. # arg1: user requested freq value # compute_cpu_freq_set() { local val case "$1" in +) val=${CPU_FREQ_cpuinfo_max} ;; -) val=${CPU_FREQ_cpuinfo_min} ;; *%) val=$((${1%?} * ${CPU_FREQ_cpuinfo_max} / 100)) ;; *[!0-9]*) log ERROR "Cannot set CPU freq to invalid value: %s" "$1" return 1 ;; "") log ERROR "Cannot set CPU freq to unspecified value" return 1 ;; *) log ERROR "Cannot set CPU freq to custom value; use +, -, or % instead" return 1 ;; esac printf "%s" "${val}" } # # Adjust CPU max scaling frequency. # set_cpu_freq_max() { local target_freq res=0 case "${CPU_SET_MAX_FREQ}" in +) target_freq=100 ;; -) target_freq=1 ;; *%) target_freq=${CPU_SET_MAX_FREQ%?} ;; *) log ERROR "Invalid CPU freq" return 1 ;; esac local pstate_info=$(printf "${CPU_PSTATE_SYSFS_PATTERN}" max_perf_pct) [ -e "${pstate_info}" ] && { log INFO "Setting intel_pstate max perf to %s" "${target_freq}%" printf "%s" "${target_freq}" > "${pstate_info}" [ $? -eq 0 ] || { log ERROR "Failed to set intel_pstate max perf" res=1 } } local cpu_index for cpu_index in $(get_online_cpus); do read_cpu_freq_info ${cpu_index} n ${CAP_CPU_FREQ_INFO} || { res=$?; continue; } target_freq=$(compute_cpu_freq_set "${CPU_SET_MAX_FREQ}") [ -z "${target_freq}" ] && { res=$?; continue; } log INFO "Setting CPU%s max scaling freq to %s Hz" ${cpu_index} "${target_freq}" [ -n "${DRY_RUN}" ] && continue printf "%s" ${target_freq} > $(print_cpu_freq_sysfs_path scaling_max ${cpu_index}) [ $? -eq 0 ] || { res=1 log ERROR "Failed to set CPU%s max scaling frequency" ${cpu_index} } done return ${res} } # # Show help message. # print_usage() { cat <