1#!/usr/bin/env bash 2# shellcheck disable=SC2013 3# shellcheck disable=SC2015 4# shellcheck disable=SC2034 5# shellcheck disable=SC2046 6# shellcheck disable=SC2059 7# shellcheck disable=SC2086 # we want word splitting 8# shellcheck disable=SC2154 9# shellcheck disable=SC2155 10# shellcheck disable=SC2162 11# shellcheck disable=SC2229 12# 13# This is an utility script to manage Intel GPU frequencies. 14# It can be used for debugging performance problems or trying to obtain a stable 15# frequency while benchmarking. 16# 17# Note the Intel i915 GPU driver allows to change the minimum, maximum and boost 18# frequencies in steps of 50 MHz via: 19# 20# /sys/class/drm/card<n>/<freq_info> 21# 22# Where <n> is the DRM card index and <freq_info> one of the following: 23# 24# - gt_max_freq_mhz (enforced maximum freq) 25# - gt_min_freq_mhz (enforced minimum freq) 26# - gt_boost_freq_mhz (enforced boost freq) 27# 28# The hardware capabilities can be accessed via: 29# 30# - gt_RP0_freq_mhz (supported maximum freq) 31# - gt_RPn_freq_mhz (supported minimum freq) 32# - gt_RP1_freq_mhz (most efficient freq) 33# 34# The current frequency can be read from: 35# - gt_act_freq_mhz (the actual GPU freq) 36# - gt_cur_freq_mhz (the last requested freq) 37# 38# Also note that in addition to GPU management, the script offers the 39# possibility to adjust CPU operating frequencies. However, this is currently 40# limited to just setting the maximum scaling frequency as percentage of the 41# maximum frequency allowed by the hardware. 42# 43# Copyright (C) 2022 Collabora Ltd. 44# Author: Cristian Ciocaltea <cristian.ciocaltea@collabora.com> 45# 46# SPDX-License-Identifier: MIT 47# 48 49# 50# Constants 51# 52 53# GPU 54DRM_FREQ_SYSFS_PATTERN="/sys/class/drm/card%d/gt_%s_freq_mhz" 55ENF_FREQ_INFO="max min boost" 56CAP_FREQ_INFO="RP0 RPn RP1" 57ACT_FREQ_INFO="act cur" 58THROTT_DETECT_SLEEP_SEC=2 59THROTT_DETECT_PID_FILE_PATH=/tmp/thrott-detect.pid 60 61# CPU 62CPU_SYSFS_PREFIX=/sys/devices/system/cpu 63CPU_PSTATE_SYSFS_PATTERN="${CPU_SYSFS_PREFIX}/intel_pstate/%s" 64CPU_FREQ_SYSFS_PATTERN="${CPU_SYSFS_PREFIX}/cpu%s/cpufreq/%s_freq" 65CAP_CPU_FREQ_INFO="cpuinfo_max cpuinfo_min" 66ENF_CPU_FREQ_INFO="scaling_max scaling_min" 67ACT_CPU_FREQ_INFO="scaling_cur" 68 69# 70# Global variables. 71# 72unset INTEL_DRM_CARD_INDEX 73unset GET_ACT_FREQ GET_ENF_FREQ GET_CAP_FREQ 74unset SET_MIN_FREQ SET_MAX_FREQ 75unset MONITOR_FREQ 76unset CPU_SET_MAX_FREQ 77unset DETECT_THROTT 78unset DRY_RUN 79 80# 81# Simple printf based stderr logger. 82# 83log() { 84 local msg_type=$1 85 86 shift 87 printf "%s: %s: " "${msg_type}" "${0##*/}" >&2 88 printf "$@" >&2 89 printf "\n" >&2 90} 91 92# 93# Helper to print sysfs path for the given card index and freq info. 94# 95# arg1: Frequency info sysfs name, one of *_FREQ_INFO constants above 96# arg2: Video card index, defaults to INTEL_DRM_CARD_INDEX 97# 98print_freq_sysfs_path() { 99 printf ${DRM_FREQ_SYSFS_PATTERN} "${2:-${INTEL_DRM_CARD_INDEX}}" "$1" 100} 101 102# 103# Helper to set INTEL_DRM_CARD_INDEX for the first identified Intel video card. 104# 105identify_intel_gpu() { 106 local i=0 vendor path 107 108 while [ ${i} -lt 16 ]; do 109 [ -c "/dev/dri/card$i" ] || { 110 i=$((i + 1)) 111 continue 112 } 113 114 path=$(print_freq_sysfs_path "" ${i}) 115 path=${path%/*}/device/vendor 116 117 [ -r "${path}" ] && read vendor < "${path}" && \ 118 [ "${vendor}" = "0x8086" ] && INTEL_DRM_CARD_INDEX=$i && return 0 119 120 i=$((i + 1)) 121 done 122 123 return 1 124} 125 126# 127# Read the specified freq info from sysfs. 128# 129# arg1: Flag (y/n) to also enable printing the freq info. 130# arg2...: Frequency info sysfs name(s), see *_FREQ_INFO constants above 131# return: Global variable(s) FREQ_${arg} containing the requested information 132# 133read_freq_info() { 134 local var val info path print=0 ret=0 135 136 [ "$1" = "y" ] && print=1 137 shift 138 139 while [ $# -gt 0 ]; do 140 info=$1 141 shift 142 var=FREQ_${info} 143 path=$(print_freq_sysfs_path "${info}") 144 145 [ -r ${path} ] && read ${var} < ${path} || { 146 log ERROR "Failed to read freq info from: %s" "${path}" 147 ret=1 148 continue 149 } 150 151 [ -n "${var}" ] || { 152 log ERROR "Got empty freq info from: %s" "${path}" 153 ret=1 154 continue 155 } 156 157 [ ${print} -eq 1 ] && { 158 eval val=\$${var} 159 printf "%6s: %4s MHz\n" "${info}" "${val}" 160 } 161 done 162 163 return ${ret} 164} 165 166# 167# Display requested info. 168# 169print_freq_info() { 170 local req_freq 171 172 [ -n "${GET_CAP_FREQ}" ] && { 173 printf "* Hardware capabilities\n" 174 read_freq_info y ${CAP_FREQ_INFO} 175 printf "\n" 176 } 177 178 [ -n "${GET_ENF_FREQ}" ] && { 179 printf "* Enforcements\n" 180 read_freq_info y ${ENF_FREQ_INFO} 181 printf "\n" 182 } 183 184 [ -n "${GET_ACT_FREQ}" ] && { 185 printf "* Actual\n" 186 read_freq_info y ${ACT_FREQ_INFO} 187 printf "\n" 188 } 189} 190 191# 192# Helper to print frequency value as requested by user via '-s, --set' option. 193# arg1: user requested freq value 194# 195compute_freq_set() { 196 local val 197 198 case "$1" in 199 +) 200 val=${FREQ_RP0} 201 ;; 202 -) 203 val=${FREQ_RPn} 204 ;; 205 *%) 206 val=$((${1%?} * FREQ_RP0 / 100)) 207 # Adjust freq to comply with 50 MHz increments 208 val=$((val / 50 * 50)) 209 ;; 210 *[!0-9]*) 211 log ERROR "Cannot set freq to invalid value: %s" "$1" 212 return 1 213 ;; 214 "") 215 log ERROR "Cannot set freq to unspecified value" 216 return 1 217 ;; 218 *) 219 # Adjust freq to comply with 50 MHz increments 220 val=$(($1 / 50 * 50)) 221 ;; 222 esac 223 224 printf "%s" "${val}" 225} 226 227# 228# Helper for set_freq(). 229# 230set_freq_max() { 231 log INFO "Setting GPU max freq to %s MHz" "${SET_MAX_FREQ}" 232 233 read_freq_info n min || return $? 234 235 [ ${SET_MAX_FREQ} -gt ${FREQ_RP0} ] && { 236 log ERROR "Cannot set GPU max freq (%s) to be greater than hw max freq (%s)" \ 237 "${SET_MAX_FREQ}" "${FREQ_RP0}" 238 return 1 239 } 240 241 [ ${SET_MAX_FREQ} -lt ${FREQ_RPn} ] && { 242 log ERROR "Cannot set GPU max freq (%s) to be less than hw min freq (%s)" \ 243 "${SET_MIN_FREQ}" "${FREQ_RPn}" 244 return 1 245 } 246 247 [ ${SET_MAX_FREQ} -lt ${FREQ_min} ] && { 248 log ERROR "Cannot set GPU max freq (%s) to be less than min freq (%s)" \ 249 "${SET_MAX_FREQ}" "${FREQ_min}" 250 return 1 251 } 252 253 [ -z "${DRY_RUN}" ] || return 0 254 255 if ! printf "%s" ${SET_MAX_FREQ} | tee $(print_freq_sysfs_path max) \ 256 $(print_freq_sysfs_path boost) > /dev/null; 257 then 258 log ERROR "Failed to set GPU max frequency" 259 return 1 260 fi 261} 262 263# 264# Helper for set_freq(). 265# 266set_freq_min() { 267 log INFO "Setting GPU min freq to %s MHz" "${SET_MIN_FREQ}" 268 269 read_freq_info n max || return $? 270 271 [ ${SET_MIN_FREQ} -gt ${FREQ_max} ] && { 272 log ERROR "Cannot set GPU min freq (%s) to be greater than max freq (%s)" \ 273 "${SET_MIN_FREQ}" "${FREQ_max}" 274 return 1 275 } 276 277 [ ${SET_MIN_FREQ} -lt ${FREQ_RPn} ] && { 278 log ERROR "Cannot set GPU min freq (%s) to be less than hw min freq (%s)" \ 279 "${SET_MIN_FREQ}" "${FREQ_RPn}" 280 return 1 281 } 282 283 [ -z "${DRY_RUN}" ] || return 0 284 285 if ! printf "%s" ${SET_MIN_FREQ} > $(print_freq_sysfs_path min); 286 then 287 log ERROR "Failed to set GPU min frequency" 288 return 1 289 fi 290} 291 292# 293# Set min or max or both GPU frequencies to the user indicated values. 294# 295set_freq() { 296 # Get hw max & min frequencies 297 read_freq_info n RP0 RPn || return $? 298 299 [ -z "${SET_MAX_FREQ}" ] || { 300 SET_MAX_FREQ=$(compute_freq_set "${SET_MAX_FREQ}") 301 [ -z "${SET_MAX_FREQ}" ] && return 1 302 } 303 304 [ -z "${SET_MIN_FREQ}" ] || { 305 SET_MIN_FREQ=$(compute_freq_set "${SET_MIN_FREQ}") 306 [ -z "${SET_MIN_FREQ}" ] && return 1 307 } 308 309 # 310 # Ensure correct operation order, to avoid setting min freq 311 # to a value which is larger than max freq. 312 # 313 # E.g.: 314 # crt_min=crt_max=600; new_min=new_max=700 315 # > operation order: max=700; min=700 316 # 317 # crt_min=crt_max=600; new_min=new_max=500 318 # > operation order: min=500; max=500 319 # 320 if [ -n "${SET_MAX_FREQ}" ] && [ -n "${SET_MIN_FREQ}" ]; then 321 [ ${SET_MAX_FREQ} -lt ${SET_MIN_FREQ} ] && { 322 log ERROR "Cannot set GPU max freq to be less than min freq" 323 return 1 324 } 325 326 read_freq_info n min || return $? 327 328 if [ ${SET_MAX_FREQ} -lt ${FREQ_min} ]; then 329 set_freq_min || return $? 330 set_freq_max 331 else 332 set_freq_max || return $? 333 set_freq_min 334 fi 335 elif [ -n "${SET_MAX_FREQ}" ]; then 336 set_freq_max 337 elif [ -n "${SET_MIN_FREQ}" ]; then 338 set_freq_min 339 else 340 log "Unexpected call to set_freq()" 341 return 1 342 fi 343} 344 345# 346# Helper for detect_throttling(). 347# 348get_thrott_detect_pid() { 349 [ -e ${THROTT_DETECT_PID_FILE_PATH} ] || return 0 350 351 local pid 352 read pid < ${THROTT_DETECT_PID_FILE_PATH} || { 353 log ERROR "Failed to read pid from: %s" "${THROTT_DETECT_PID_FILE_PATH}" 354 return 1 355 } 356 357 local proc_path=/proc/${pid:-invalid}/cmdline 358 [ -r ${proc_path} ] && grep -qs "${0##*/}" ${proc_path} && { 359 printf "%s" "${pid}" 360 return 0 361 } 362 363 # Remove orphaned PID file 364 rm -rf ${THROTT_DETECT_PID_FILE_PATH} 365 return 1 366} 367 368# 369# Control detection and reporting of GPU throttling events. 370# arg1: start - run throttle detector in background 371# stop - stop throttle detector process, if any 372# status - verify if throttle detector is running 373# 374detect_throttling() { 375 local pid 376 pid=$(get_thrott_detect_pid) 377 378 case "$1" in 379 status) 380 printf "Throttling detector is " 381 [ -z "${pid}" ] && printf "not running\n" && return 0 382 printf "running (pid=%s)\n" ${pid} 383 ;; 384 385 stop) 386 [ -z "${pid}" ] && return 0 387 388 log INFO "Stopping throttling detector (pid=%s)" "${pid}" 389 kill ${pid}; sleep 1; kill -0 ${pid} 2>/dev/null && kill -9 ${pid} 390 rm -rf ${THROTT_DETECT_PID_FILE_PATH} 391 ;; 392 393 start) 394 [ -n "${pid}" ] && { 395 log WARN "Throttling detector is already running (pid=%s)" ${pid} 396 return 0 397 } 398 399 ( 400 read_freq_info n RPn || exit $? 401 402 while true; do 403 sleep ${THROTT_DETECT_SLEEP_SEC} 404 read_freq_info n act min cur || exit $? 405 406 # 407 # The throttling seems to occur when act freq goes below min. 408 # However, it's necessary to exclude the idle states, where 409 # act freq normally reaches RPn and cur goes below min. 410 # 411 [ ${FREQ_act} -lt ${FREQ_min} ] && \ 412 [ ${FREQ_act} -gt ${FREQ_RPn} ] && \ 413 [ ${FREQ_cur} -ge ${FREQ_min} ] && \ 414 printf "GPU throttling detected: act=%s min=%s cur=%s RPn=%s\n" \ 415 ${FREQ_act} ${FREQ_min} ${FREQ_cur} ${FREQ_RPn} 416 done 417 ) & 418 419 pid=$! 420 log INFO "Started GPU throttling detector (pid=%s)" ${pid} 421 422 printf "%s\n" ${pid} > ${THROTT_DETECT_PID_FILE_PATH} || \ 423 log WARN "Failed to write throttle detector PID file" 424 ;; 425 esac 426} 427 428# 429# Retrieve the list of online CPUs. 430# 431get_online_cpus() { 432 local path cpu_index 433 434 printf "0" 435 for path in $(grep 1 ${CPU_SYSFS_PREFIX}/cpu*/online); do 436 cpu_index=${path##*/cpu} 437 printf " %s" ${cpu_index%%/*} 438 done 439} 440 441# 442# Helper to print sysfs path for the given CPU index and freq info. 443# 444# arg1: Frequency info sysfs name, one of *_CPU_FREQ_INFO constants above 445# arg2: CPU index 446# 447print_cpu_freq_sysfs_path() { 448 printf ${CPU_FREQ_SYSFS_PATTERN} "$2" "$1" 449} 450 451# 452# Read the specified CPU freq info from sysfs. 453# 454# arg1: CPU index 455# arg2: Flag (y/n) to also enable printing the freq info. 456# arg3...: Frequency info sysfs name(s), see *_CPU_FREQ_INFO constants above 457# return: Global variable(s) CPU_FREQ_${arg} containing the requested information 458# 459read_cpu_freq_info() { 460 local var val info path cpu_index print=0 ret=0 461 462 cpu_index=$1 463 [ "$2" = "y" ] && print=1 464 shift 2 465 466 while [ $# -gt 0 ]; do 467 info=$1 468 shift 469 var=CPU_FREQ_${info} 470 path=$(print_cpu_freq_sysfs_path "${info}" ${cpu_index}) 471 472 [ -r ${path} ] && read ${var} < ${path} || { 473 log ERROR "Failed to read CPU freq info from: %s" "${path}" 474 ret=1 475 continue 476 } 477 478 [ -n "${var}" ] || { 479 log ERROR "Got empty CPU freq info from: %s" "${path}" 480 ret=1 481 continue 482 } 483 484 [ ${print} -eq 1 ] && { 485 eval val=\$${var} 486 printf "%6s: %4s Hz\n" "${info}" "${val}" 487 } 488 done 489 490 return ${ret} 491} 492 493# 494# Helper to print freq. value as requested by user via '--cpu-set-max' option. 495# arg1: user requested freq value 496# 497compute_cpu_freq_set() { 498 local val 499 500 case "$1" in 501 +) 502 val=${CPU_FREQ_cpuinfo_max} 503 ;; 504 -) 505 val=${CPU_FREQ_cpuinfo_min} 506 ;; 507 *%) 508 val=$((${1%?} * CPU_FREQ_cpuinfo_max / 100)) 509 ;; 510 *[!0-9]*) 511 log ERROR "Cannot set CPU freq to invalid value: %s" "$1" 512 return 1 513 ;; 514 "") 515 log ERROR "Cannot set CPU freq to unspecified value" 516 return 1 517 ;; 518 *) 519 log ERROR "Cannot set CPU freq to custom value; use +, -, or % instead" 520 return 1 521 ;; 522 esac 523 524 printf "%s" "${val}" 525} 526 527# 528# Adjust CPU max scaling frequency. 529# 530set_cpu_freq_max() { 531 local target_freq res=0 532 case "${CPU_SET_MAX_FREQ}" in 533 +) 534 target_freq=100 535 ;; 536 -) 537 target_freq=1 538 ;; 539 *%) 540 target_freq=${CPU_SET_MAX_FREQ%?} 541 ;; 542 *) 543 log ERROR "Invalid CPU freq" 544 return 1 545 ;; 546 esac 547 548 local pstate_info=$(printf "${CPU_PSTATE_SYSFS_PATTERN}" max_perf_pct) 549 [ -e "${pstate_info}" ] && { 550 log INFO "Setting intel_pstate max perf to %s" "${target_freq}%" 551 if ! printf "%s" "${target_freq}" > "${pstate_info}"; 552 then 553 log ERROR "Failed to set intel_pstate max perf" 554 res=1 555 fi 556 } 557 558 local cpu_index 559 for cpu_index in $(get_online_cpus); do 560 read_cpu_freq_info ${cpu_index} n ${CAP_CPU_FREQ_INFO} || { res=$?; continue; } 561 562 target_freq=$(compute_cpu_freq_set "${CPU_SET_MAX_FREQ}") 563 tf_res=$? 564 [ -z "${target_freq}" ] && { res=$tf_res; continue; } 565 566 log INFO "Setting CPU%s max scaling freq to %s Hz" ${cpu_index} "${target_freq}" 567 [ -n "${DRY_RUN}" ] && continue 568 569 if ! printf "%s" ${target_freq} > $(print_cpu_freq_sysfs_path scaling_max ${cpu_index}); 570 then 571 res=1 572 log ERROR "Failed to set CPU%s max scaling frequency" ${cpu_index} 573 fi 574 done 575 576 return ${res} 577} 578 579# 580# Show help message. 581# 582print_usage() { 583 cat <<EOF 584Usage: ${0##*/} [OPTION]... 585 586A script to manage Intel GPU frequencies. Can be used for debugging performance 587problems or trying to obtain a stable frequency while benchmarking. 588 589Note Intel GPUs only accept specific frequencies, usually multiples of 50 MHz. 590 591Options: 592 -g, --get [act|enf|cap|all] 593 Get frequency information: active (default), enforced, 594 hardware capabilities or all of them. 595 596 -s, --set [{min|max}=]{FREQUENCY[%]|+|-} 597 Set min or max frequency to the given value (MHz). 598 Append '%' to interpret FREQUENCY as % of hw max. 599 Use '+' or '-' to set frequency to hardware max or min. 600 Omit min/max prefix to set both frequencies. 601 602 -r, --reset Reset frequencies to hardware defaults. 603 604 -m, --monitor [act|enf|cap|all] 605 Monitor the indicated frequencies via 'watch' utility. 606 See '-g, --get' option for more details. 607 608 -d|--detect-thrott [start|stop|status] 609 Start (default operation) the throttling detector 610 as a background process. Use 'stop' or 'status' to 611 terminate the detector process or verify its status. 612 613 --cpu-set-max [FREQUENCY%|+|-} 614 Set CPU max scaling frequency as % of hw max. 615 Use '+' or '-' to set frequency to hardware max or min. 616 617 -r, --reset Reset frequencies to hardware defaults. 618 619 --dry-run See what the script will do without applying any 620 frequency changes. 621 622 -h, --help Display this help text and exit. 623EOF 624} 625 626# 627# Parse user input for '-g, --get' option. 628# Returns 0 if a value has been provided, otherwise 1. 629# 630parse_option_get() { 631 local ret=0 632 633 case "$1" in 634 act) GET_ACT_FREQ=1;; 635 enf) GET_ENF_FREQ=1;; 636 cap) GET_CAP_FREQ=1;; 637 all) GET_ACT_FREQ=1; GET_ENF_FREQ=1; GET_CAP_FREQ=1;; 638 -*|"") 639 # No value provided, using default. 640 GET_ACT_FREQ=1 641 ret=1 642 ;; 643 *) 644 print_usage 645 exit 1 646 ;; 647 esac 648 649 return ${ret} 650} 651 652# 653# Validate user input for '-s, --set' option. 654# arg1: input value to be validated 655# arg2: optional flag indicating input is restricted to % 656# 657validate_option_set() { 658 case "$1" in 659 +|-|[0-9]%|[0-9][0-9]%) 660 return 0 661 ;; 662 *[!0-9]*|"") 663 print_usage 664 exit 1 665 ;; 666 esac 667 668 [ -z "$2" ] || { print_usage; exit 1; } 669} 670 671# 672# Parse script arguments. 673# 674[ $# -eq 0 ] && { print_usage; exit 1; } 675 676while [ $# -gt 0 ]; do 677 case "$1" in 678 -g|--get) 679 parse_option_get "$2" && shift 680 ;; 681 682 -s|--set) 683 shift 684 case "$1" in 685 min=*) 686 SET_MIN_FREQ=${1#min=} 687 validate_option_set "${SET_MIN_FREQ}" 688 ;; 689 max=*) 690 SET_MAX_FREQ=${1#max=} 691 validate_option_set "${SET_MAX_FREQ}" 692 ;; 693 *) 694 SET_MIN_FREQ=$1 695 validate_option_set "${SET_MIN_FREQ}" 696 SET_MAX_FREQ=${SET_MIN_FREQ} 697 ;; 698 esac 699 ;; 700 701 -r|--reset) 702 RESET_FREQ=1 703 SET_MIN_FREQ="-" 704 SET_MAX_FREQ="+" 705 ;; 706 707 -m|--monitor) 708 MONITOR_FREQ=act 709 parse_option_get "$2" && MONITOR_FREQ=$2 && shift 710 ;; 711 712 -d|--detect-thrott) 713 DETECT_THROTT=start 714 case "$2" in 715 start|stop|status) 716 DETECT_THROTT=$2 717 shift 718 ;; 719 esac 720 ;; 721 722 --cpu-set-max) 723 shift 724 CPU_SET_MAX_FREQ=$1 725 validate_option_set "${CPU_SET_MAX_FREQ}" restricted 726 ;; 727 728 --dry-run) 729 DRY_RUN=1 730 ;; 731 732 -h|--help) 733 print_usage 734 exit 0 735 ;; 736 737 *) 738 print_usage 739 exit 1 740 ;; 741 esac 742 743 shift 744done 745 746# 747# Main 748# 749RET=0 750 751identify_intel_gpu || { 752 log INFO "No Intel GPU detected" 753 exit 0 754} 755 756[ -n "${SET_MIN_FREQ}${SET_MAX_FREQ}" ] && { set_freq || RET=$?; } 757print_freq_info 758 759[ -n "${DETECT_THROTT}" ] && detect_throttling ${DETECT_THROTT} 760 761[ -n "${CPU_SET_MAX_FREQ}" ] && { set_cpu_freq_max || RET=$?; } 762 763[ -n "${MONITOR_FREQ}" ] && { 764 log INFO "Entering frequency monitoring mode" 765 sleep 2 766 exec watch -d -n 1 "$0" -g "${MONITOR_FREQ}" 767} 768 769exit ${RET} 770