• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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