לדלג לתוכן

5.4 ניטור וניהול NVML ו nvidia smi פתרון

פתרון - ניטור וניהול - NVML ו-nvidia-smi

הערה: כל הפלטים למטה הם ממכונת H100 SXM עם driver 550.x ו-CUDA 12.4. המספרים המדויקים - הספק, טמפרטורה, תדרים, תקרות - ישתנו אצלכם לפי הכרטיס, הקירור והעומס הרגעי (על T4 התקרה היא ~70W והתדרים נמוכים בהרבה). מה שלא ישתנה הוא המבנה והמגמה: תחת עומס ההספק עולה, נוגע בתקרה, והתדר יורד; וסיבת הוויסות SW Power Cap נדלקת. אם אין לכם הרשאת root, תרגילי הניהול (4) ייכשלו ב-Insufficient Permissions - זה תקין, והסבר הצפוי מופיע בפתרון.

פתרון תרגיל 1 - תשאול בסיסי עם query-gpu

nvidia-smi --query-gpu=name,power.draw,power.limit,temperature.gpu,utilization.gpu,memory.used,memory.total,clocks.sm,clocks.mem,pstate --format=csv

הפלט הצפוי (בסרק):

name, power.draw [W], power.limit [W], temperature.gpu, utilization.gpu [%], memory.used [MiB], memory.total [MiB], clocks.current.sm [MHz], clocks.current.memory [MHz], pstate
NVIDIA H100 80GB HBM3, 78.11, 700.00, 34, 0, 512, 81559, 345, 2619, P0

עם noheader,nounits:

nvidia-smi --query-gpu=power.draw,temperature.gpu,clocks.sm --format=csv,noheader,nounits
78.11, 34, 345

הטווח המותר של תקרת ההספק:

nvidia-smi -q -d POWER | grep -i "power limit"
        Current Power Limit               : 700.00 W
        Requested Power Limit             : 700.00 W
        Default Power Limit               : 700.00 W
        Min Power Limit                   : 200.00 W
        Max Power Limit                   : 700.00 W

למה זה עבד: --query-gpu מבקש מ-NVML שדות מדויקים ומחזיר אותם כ-CSV במקום כטבלה. הגרסה noheader,nounits מסירה את הכותרת ואת היחידות ומשאירה ערכים נקיים - הרבה יותר נוח ל-awk או לפרסור בסקריפט. תקרת ברירת המחדל של H100 SXM היא 700W, והטווח המותר הוא 200-700W; ניסיון לקבוע מחוץ לטווח יידחה.

איך להכליל: --query-gpu הוא הדרך הנכונה ל-one-liner אינטראקטיבי או לניטור מהיר - הרבה יותר יציב מפרסור הטבלה. אבל לכלי production אמיתי עדיין עדיף NVML ישירות (ראו תרגיל 5). את רשימת כל השדות הזמינים מקבלים עם nvidia-smi --help-query-gpu.

פתרון תרגיל 2 - צפייה חיה בהספק ותדר עם dmon

טרמינל 1:

./burn 30

טרמינל 2:

nvidia-smi dmon -s puct -d 1

הפלט הצפוי (מקוצר):

# gpu   pwr gtemp mtemp    sm   mem   enc   dec   mclk   pclk   rxpci   txpci
# Idx     W     C     C     %     %     %     %    MHz    MHz    MB/s    MB/s
    0    79    35     -     0     0     0     0   2619    345       0       0
    0   688    56     -    99    41     0     0   2619   1980       0       0
    0   700    62     -    99    44     0     0   2619   1875       0       0
    0   700    67     -    99    45     0     0   2619   1785       0       0
    0   700    70     -    99    45     0     0   2619   1755       0       0

טרמינל 3:

nvidia-smi pmon -s um -d 1
# gpu        pid  type    sm   mem   enc   dec   command
# Idx          #   C/G     %     %     %     %   name
    0      48213     C    99    41     -     -   burn

וסיבות הוויסות בשיא:

nvidia-smi -q -d PERFORMANCE | grep -A1 "SW Power Cap"
        SW Power Cap                      : Active
        HW Slowdown                       : Not Active

למה זה עבד: ה-burn הוא kernel compute-bound (לולאת FMA צפופה, בלי גישות זיכרון בלולאה), ולכן הוא דוחף את יחידות ה-FP32 עד הסוף. בשנייה הראשונה התדר בבוסט המלא (1980 MHz, 688W), אבל תוך שניות ההספק נוגע בתקרת 700W, SW Power Cap נדלק, והכרטיס מוריד תדר ל-1755 MHz כדי להישאר בתקציב. זהו ויסות (throttling) בזמן אמת: הכרטיס לא יכול להריץ 1980 MHz בתוך 700W עבור העומס הזה, אז הוא מוריד תדר. pmon הראה שה-PID של burn הוא זה שאחראי ל-99% ניצול ה-SM.

איך להכליל: dmon היא העין הראשונה לכל חקירת ביצועים ברמת מערכת - היא מראה מיד אם אתם power-bound (התדר יורד כשההספק בתקרה) או thermal-bound (gtemp מטפס גבוה ו-HW Thermal Slowdown נדלק). pmon מייחס את הצריכה לתהליך ספציפי, קריטי כשכמה jobs חולקים כרטיס. תמיד הצליבו את dmon עם -q -d PERFORMANCE כדי לדעת למה התדר יורד.

פתרון תרגיל 3 - סקריפט pynvml שדוגם workload

# sampler.py
import time
import pynvml as nv

def summarize(values):
    return min(values), sum(values) / len(values), max(values)

nv.nvmlInit()
try:
    h = nv.nvmlDeviceGetHandleByIndex(0)
    limit_w = nv.nvmlDeviceGetPowerManagementLimit(h) / 1000.0

    powers, temps, utils, clocks = [], [], [], []
    duration_s, dt = 20.0, 0.1
    t_end = time.time() + duration_s
    while time.time() < t_end:
        powers.append(nv.nvmlDeviceGetPowerUsage(h) / 1000.0)          # mW -> W
        temps.append(nv.nvmlDeviceGetTemperature(h, nv.NVML_TEMPERATURE_GPU))
        utils.append(nv.nvmlDeviceGetUtilizationRates(h).gpu)
        clocks.append(nv.nvmlDeviceGetClockInfo(h, nv.NVML_CLOCK_SM))
        time.sleep(dt)

    p_min, p_avg, p_max = summarize(powers)
    t_min, t_avg, t_max = summarize(temps)
    u_min, u_avg, u_max = summarize(utils)
    c_min, c_avg, c_max = summarize(clocks)

    print(f"Samples:      {len(powers)}")
    print(f"Power  W:     avg {p_avg:6.1f}   max {p_max:6.1f}   (limit {limit_w:.0f})")
    print(f"Temp  C:      avg {t_avg:6.1f}   max {t_max:6.1f}")
    print(f"Util %:       avg {u_avg:6.1f}")
    print(f"SM clock MHz: min {c_min}   max {c_max}")
    print(f"Power/limit ratio: {p_avg / limit_w:.2%}")
finally:
    nv.nvmlShutdown()

הרצה במקביל ל-./burn 20:

python sampler.py

הפלט הצפוי:

Samples:      200
Power  W:     avg  695.4   max  700.1   (limit 700)
Temp  C:      avg   64.8   max   71.0
Util %:       avg   98.7
SM clock MHz: min 1755   max 1980
Power/limit ratio: 99.34%

למה זה עבד: הסקריפט דוגם את NVML ישירות דרך pynvml כל 100ms וצובר את הערכים. שמנו לב ליחידות: nvmlDeviceGetPowerUsage מחזיר מיליוואט (חילקנו ב-1000), nvmlDeviceGetClockInfo מחזיר MHz, ו-nvmlDeviceGetUtilizationRates מחזיר אובייקט עם .gpu. הפער בין תדר ה-SM המקסימלי (1980, בהתחלה כשהכרטיס קר) למינימלי (1755, אחרי חימום וגעילה בתקרה) הוא בדיוק הוויסות שראינו ב-dmon, עכשיו נמדד תוכנתית. יחס ההספק/תקרה של ~99% מאשר שה-workload דוחף כמעט צמוד לתקרה - כלומר power-bound.

איך להכליל: זהו השלד של כל collector של טלמטריה: nvmlInit -> ידית -> לולאת דגימה שצוברת -> nvmlShutdown ב-finally. מכאן קצרה הדרך ל-exporter אמיתי (תרגיל 6). כלל ברזל: תמיד סוגרים עם nvmlShutdown(), ותמיד זוכרים את היחידות (mW להספק, בתים לזיכרון) - טעות יחידות היא הבאג הנפוץ ביותר בקוד NVML.

פתרון תרגיל 4 - תקרת הספק והשפעתה על התפוקה

התקרה הנוכחית:

nvidia-smi -q -d POWER | grep -i "power limit"
        Current Power Limit               : 700.00 W
        Min Power Limit                   : 200.00 W
        Max Power Limit                   : 700.00 W

מדידה בתקרה מלאה, ואז בתקרה מופחתת (עם root):

time ./burn 5                         # 700W limit

sudo nvidia-smi -pm 1                 # persistence mode
sudo nvidia-smi -pl 350               # lower the limit by half
time ./burn 5                         # again, now at 350W
nvidia-smi dmon -s pc -c 5            # 5 samples of power and clock

הפלט הצפוי:

# at 700W limit:
real    0m5.12s
#  dmon:  pwr ~700W,  pclk ~1785 MHz

# after  -pl 350:
Power limit for GPU 00000000:1B:00.0 was set to 350.00 W from 700.00 W.
real    0m9.40s
# gpu   pwr   pclk
# Idx     W    MHz
    0   349    990
    0   350    975
    0   350    975
    0   350    960
    0   350    960

והחזרת התקרה:

sudo nvidia-smi -pl 700
Power limit for GPU 00000000:1B:00.0 was set to 700.00 W from 350.00 W.

אם אין לכם root, במקום המדידה תקבלו:

Insufficient Permissions. Please try running as root or administrator.

למה זה עבד: תקרת ההספק היא תקציב חשמלי קשיח. ב-700W הכרטיס מריץ את ה-burn בתדר ~1785 MHz וגומר בערך תוך 5 שניות. כשהורדנו את התקרה ל-350W, הכרטיס חייב להישאר מתחת ל-350W, ולכן הוריד את התדר ל-~975 MHz (בערך חצי), וה-burn לקח כמעט כפול זמן (9.4 שניות). זהו הקשר הישיר: פחות ואטים -> פחות מחזורי שעון לשנייה -> פחות FLOP לשנייה. בענן משותף ההרשאה חסומה בכוונה, ואז מקבלים Insufficient Permissions - וזה תקין, פשוט לא ניתן לשנות תקרה במכונה שלא שלכם.

איך להכליל: תקרת ההספק היא הכפתור הישיר שמחבר בין תקציב חשמלי לביצועים. במרכזי נתונים מגבילים תקרות כדי להישאר בתוך תקציב חשמל/קירור של המתלה - ומשלמים בביצועים. בדיוק בגלל זה, לפני benchmark מהימן נועלים תדר (-lgc) כדי להסיר את המשתנה הזה, כפי שנראה בפרק 9.1. ותמיד מחזירים את התקרה בסוף - הגדרה שנשכחת נשארת עד reset או reboot.

פתרון תרגיל 5 - למה NVML ולא טקסט של nvidia-smi

הסקריפט השביר (מפרסר טקסט):

# power_bad.sh
nvidia-smi | grep -oP '\d+W / ' | grep -oP '\d+'
78

הסקריפט היציב (pynvml):

# power_good.py
import pynvml as nv
nv.nvmlInit()
try:
    h = nv.nvmlDeviceGetHandleByIndex(0)
    print(int(nv.nvmlDeviceGetPowerUsage(h) / 1000))     # mW -> W
finally:
    nv.nvmlShutdown()
78

שניהם מסכימים על 78W. אבל ההבדל מהותי:

למה זה עבד: שני הסקריפטים מגיעים לאותו מספר כי בסוף שניהם קוראים את אותו ערך מ-NVML - nvidia-smi הוא בסך הכל עטיפה מעל libnvml.so. אבל הסקריפט הטקסטואלי מסתמך על מבנה הפלט המדויק: המחרוזת W /, המיקום, רוחב העמודה. הגלוסרי אומר במפורש שפורמט הטקסט של nvidia-smi אינו מובטח כיציב - עדכון driver יכול לשנות את הטבלה (סדר שדות, רווחים, מחרוזות) ולשבור את ה-grep שלכם בלי שנגעתם בשורת קוד אחת. הסקריפט ה-pynvml מדבר עם ה-API היציב ישירות, ולכן חסין לשינויי פורמט.

איך להכליל: שלוש רמות איכות. גרוע: nvidia-smi | grep | awk (שביר, ישבר בעדכון driver, ומריץ תת-תהליך בכל דגימה). ביניים מקובל: nvidia-smi -x (XML מובנה) או --query-gpu ... --format=csv - עדיין דרך nvidia-smi, אבל מובנה. הטוב ביותר: C API של NVML, או pynvml/nvml_wrapper - ה-API שהמקור מבטיח את יציבותו, זול לקריאה, ובלי תת-תהליכים. כל כלי ניטור אמיתי (dashboard, exporter, מתזמן) חייב לפנות ל-NVML.

פתרון תרגיל 6 (בונוס) - exporter קטן וזיהוי throttling

# exporter.py
import time
import pynvml as nv

nv.nvmlInit()
try:
    h = nv.nvmlDeviceGetHandleByIndex(0)
    print("timestamp,power_w,temp_c,sm_clock,throttle")
    while True:
        power = nv.nvmlDeviceGetPowerUsage(h) / 1000.0
        temp  = nv.nvmlDeviceGetTemperature(h, nv.NVML_TEMPERATURE_GPU)
        clk   = nv.nvmlDeviceGetClockInfo(h, nv.NVML_CLOCK_SM)
        reasons = nv.nvmlDeviceGetCurrentClocksThrottleReasons(h)

        tags = []
        if reasons & nv.nvmlClocksThrottleReasonSwPowerCap:
            tags.append("SwPowerCap")
        if reasons & nv.nvmlClocksThrottleReasonHwThermalSlowdown:
            tags.append("HwThermal")
        if reasons & nv.nvmlClocksThrottleReasonSwThermalSlowdown:
            tags.append("SwThermal")
        throttle = "|".join(tags) if tags else "none"

        print(f"{time.time():.0f},{power:.1f},{temp},{clk},{throttle}")
        time.sleep(1.0)
finally:
    nv.nvmlShutdown()

הרצה במקביל ל-./burn 15:

timestamp,power_w,temp_c,sm_clock,throttle
1720598400,79.0,35,345,none
1720598401,688.3,56,1980,none
1720598402,700.1,62,1875,SwPowerCap
1720598403,700.0,67,1785,SwPowerCap
1720598404,700.0,70,1755,SwPowerCap
1720598405,120.5,58,1410,none

למה זה עבד: nvmlDeviceGetCurrentClocksThrottleReasons מחזיר מסכת סיביות, וכל סיבה נבדקת עם AND מול הקבוע שלה. בשנייה הראשונה של העומס אין ויסות (none, התדר בבוסט 1980), אבל ברגע שההספק נוגע ב-700W הביט SwPowerCap נדלק והתדר יורד ל-1755 - זיהוי תוכנתי מדויק של אותו סיפור שראינו ב-dmon ו--q -d PERFORMANCE. כשה-burn נגמר, ההספק צונח והוויסות נעלם (none).

איך להכליל: זהו גרעין של exporter לפרודקשן (למשל DCGM-exporter ל-Prometheus בנוי בדיוק על העיקרון הזה, מעל NVML/DCGM). היתרון של NVML כאן כפול: ה-API יציב לאורך גרסאות driver, וקריאותיו זולות ובתוך התהליך - בלי להריץ nvidia-smi כתת-תהליך בכל שנייה. והקשר לפרק 7.1: אם SwPowerCap דולק תחת עומס, ה-workload power-bound, ואז אופטימיזציה של הקוד לא תשבור את תקרת ה-700W - התדר יישאר מוגבל. מה שיעזור זה תקציב הספק/קירור גדול יותר (power ingress ו-heat egress), או הפחתת כמות העבודה. תפוקת השיא בפועל של הכרטיס אינה קבועה - היא נקבעת על ידי כמה חשמל אפשר להכניס וכמה חום אפשר להוציא, בדיוק הנושא שנעמיק בו בפרק 7.1.