nvidia_smi.chart.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. # -*- coding: utf-8 -*-
  2. # Description: nvidia-smi netdata python.d module
  3. # Original Author: Steven Noonan (tycho)
  4. # Author: Ilya Mashchenko (ilyam8)
  5. # User Memory Stat Author: Guido Scatena (scatenag)
  6. import os
  7. import pwd
  8. import subprocess
  9. import threading
  10. import xml.etree.ElementTree as et
  11. from bases.FrameworkServices.SimpleService import SimpleService
  12. from bases.collection import find_binary
  13. disabled_by_default = True
  14. NVIDIA_SMI = 'nvidia-smi'
  15. NOT_AVAILABLE = 'N/A'
  16. EMPTY_ROW = ''
  17. EMPTY_ROW_LIMIT = 500
  18. POLLER_BREAK_ROW = '</nvidia_smi_log>'
  19. PCI_BANDWIDTH = 'pci_bandwidth'
  20. PCI_BANDWIDTH_PERCENT = 'pci_bandwidth_percent'
  21. FAN_SPEED = 'fan_speed'
  22. GPU_UTIL = 'gpu_utilization'
  23. MEM_UTIL = 'mem_utilization'
  24. ENCODER_UTIL = 'encoder_utilization'
  25. MEM_USAGE = 'mem_usage'
  26. BAR_USAGE = 'bar1_mem_usage'
  27. TEMPERATURE = 'temperature'
  28. CLOCKS = 'clocks'
  29. POWER = 'power'
  30. POWER_STATE = 'power_state'
  31. PROCESSES_MEM = 'processes_mem'
  32. USER_MEM = 'user_mem'
  33. USER_NUM = 'user_num'
  34. ORDER = [
  35. PCI_BANDWIDTH,
  36. PCI_BANDWIDTH_PERCENT,
  37. FAN_SPEED,
  38. GPU_UTIL,
  39. MEM_UTIL,
  40. ENCODER_UTIL,
  41. MEM_USAGE,
  42. BAR_USAGE,
  43. TEMPERATURE,
  44. CLOCKS,
  45. POWER,
  46. POWER_STATE,
  47. PROCESSES_MEM,
  48. USER_MEM,
  49. USER_NUM,
  50. ]
  51. # https://docs.nvidia.com/gameworks/content/gameworkslibrary/coresdk/nvapi/group__gpupstate.html
  52. POWER_STATES = ['P' + str(i) for i in range(0, 16)]
  53. # PCI Transfer data rate in gigabits per second (Gb/s) per generation
  54. PCI_SPEED = {
  55. "1": 2.5,
  56. "2": 5,
  57. "3": 8,
  58. "4": 16,
  59. "5": 32
  60. }
  61. # PCI encoding per generation
  62. PCI_ENCODING = {
  63. "1": 2 / 10,
  64. "2": 2 / 10,
  65. "3": 2 / 130,
  66. "4": 2 / 130,
  67. "5": 2 / 130
  68. }
  69. def gpu_charts(gpu):
  70. fam = gpu.full_name()
  71. charts = {
  72. PCI_BANDWIDTH: {
  73. 'options': [None, 'PCI Express Bandwidth Utilization', 'KiB/s', fam, 'nvidia_smi.pci_bandwidth', 'area'],
  74. 'lines': [
  75. ['rx_util', 'rx', 'absolute', 1, 1],
  76. ['tx_util', 'tx', 'absolute', 1, -1],
  77. ]
  78. },
  79. PCI_BANDWIDTH_PERCENT: {
  80. 'options': [None, 'PCI Express Bandwidth Percent', 'percentage', fam, 'nvidia_smi.pci_bandwidth_percent',
  81. 'area'],
  82. 'lines': [
  83. ['rx_util_percent', 'rx_percent'],
  84. ['tx_util_percent', 'tx_percent'],
  85. ]
  86. },
  87. FAN_SPEED: {
  88. 'options': [None, 'Fan Speed', 'percentage', fam, 'nvidia_smi.fan_speed', 'line'],
  89. 'lines': [
  90. ['fan_speed', 'speed'],
  91. ]
  92. },
  93. GPU_UTIL: {
  94. 'options': [None, 'GPU Utilization', 'percentage', fam, 'nvidia_smi.gpu_utilization', 'line'],
  95. 'lines': [
  96. ['gpu_util', 'utilization'],
  97. ]
  98. },
  99. MEM_UTIL: {
  100. 'options': [None, 'Memory Bandwidth Utilization', 'percentage', fam, 'nvidia_smi.mem_utilization', 'line'],
  101. 'lines': [
  102. ['memory_util', 'utilization'],
  103. ]
  104. },
  105. ENCODER_UTIL: {
  106. 'options': [None, 'Encoder/Decoder Utilization', 'percentage', fam, 'nvidia_smi.encoder_utilization',
  107. 'line'],
  108. 'lines': [
  109. ['encoder_util', 'encoder'],
  110. ['decoder_util', 'decoder'],
  111. ]
  112. },
  113. MEM_USAGE: {
  114. 'options': [None, 'Memory Usage', 'MiB', fam, 'nvidia_smi.memory_allocated', 'stacked'],
  115. 'lines': [
  116. ['fb_memory_free', 'free'],
  117. ['fb_memory_used', 'used'],
  118. ]
  119. },
  120. BAR_USAGE: {
  121. 'options': [None, 'Bar1 Memory Usage', 'MiB', fam, 'nvidia_smi.bar1_memory_usage', 'stacked'],
  122. 'lines': [
  123. ['bar1_memory_free', 'free'],
  124. ['bar1_memory_used', 'used'],
  125. ]
  126. },
  127. TEMPERATURE: {
  128. 'options': [None, 'Temperature', 'celsius', fam, 'nvidia_smi.temperature', 'line'],
  129. 'lines': [
  130. ['gpu_temp', 'temp'],
  131. ]
  132. },
  133. CLOCKS: {
  134. 'options': [None, 'Clock Frequencies', 'MHz', fam, 'nvidia_smi.clocks', 'line'],
  135. 'lines': [
  136. ['graphics_clock', 'graphics'],
  137. ['video_clock', 'video'],
  138. ['sm_clock', 'sm'],
  139. ['mem_clock', 'mem'],
  140. ]
  141. },
  142. POWER: {
  143. 'options': [None, 'Power Utilization', 'Watts', fam, 'nvidia_smi.power', 'line'],
  144. 'lines': [
  145. ['power_draw', 'power', 'absolute', 1, 100],
  146. ]
  147. },
  148. POWER_STATE: {
  149. 'options': [None, 'Power State', 'state', fam, 'nvidia_smi.power_state', 'line'],
  150. 'lines': [['power_state_' + v.lower(), v, 'absolute'] for v in POWER_STATES]
  151. },
  152. PROCESSES_MEM: {
  153. 'options': [None, 'Memory Used by Each Process', 'MiB', fam, 'nvidia_smi.processes_mem', 'stacked'],
  154. 'lines': []
  155. },
  156. USER_MEM: {
  157. 'options': [None, 'Memory Used by Each User', 'MiB', fam, 'nvidia_smi.user_mem', 'stacked'],
  158. 'lines': []
  159. },
  160. USER_NUM: {
  161. 'options': [None, 'Number of User on GPU', 'num', fam, 'nvidia_smi.user_num', 'line'],
  162. 'lines': [
  163. ['user_num', 'users'],
  164. ]
  165. },
  166. }
  167. idx = gpu.num
  168. order = ['gpu{0}_{1}'.format(idx, v) for v in ORDER]
  169. charts = dict(('gpu{0}_{1}'.format(idx, k), v) for k, v in charts.items())
  170. for chart in charts.values():
  171. for line in chart['lines']:
  172. line[0] = 'gpu{0}_{1}'.format(idx, line[0])
  173. return order, charts
  174. class NvidiaSMI:
  175. def __init__(self):
  176. self.command = find_binary(NVIDIA_SMI)
  177. self.active_proc = None
  178. def run_once(self):
  179. proc = subprocess.Popen([self.command, '-x', '-q'], stdout=subprocess.PIPE)
  180. stdout, _ = proc.communicate()
  181. return stdout
  182. def run_loop(self, interval):
  183. if self.active_proc:
  184. self.kill()
  185. proc = subprocess.Popen([self.command, '-x', '-q', '-l', str(interval)], stdout=subprocess.PIPE)
  186. self.active_proc = proc
  187. return proc.stdout
  188. def kill(self):
  189. if self.active_proc:
  190. self.active_proc.kill()
  191. self.active_proc = None
  192. class NvidiaSMIPoller(threading.Thread):
  193. def __init__(self, poll_interval):
  194. threading.Thread.__init__(self)
  195. self.daemon = True
  196. self.smi = NvidiaSMI()
  197. self.interval = poll_interval
  198. self.lock = threading.RLock()
  199. self.last_data = str()
  200. self.exit = False
  201. self.empty_rows = 0
  202. self.rows = list()
  203. def has_smi(self):
  204. return bool(self.smi.command)
  205. def run_once(self):
  206. return self.smi.run_once()
  207. def run(self):
  208. out = self.smi.run_loop(self.interval)
  209. for row in out:
  210. if self.exit or self.empty_rows > EMPTY_ROW_LIMIT:
  211. break
  212. self.process_row(row)
  213. self.smi.kill()
  214. def process_row(self, row):
  215. row = row.decode()
  216. self.empty_rows += (row == EMPTY_ROW)
  217. self.rows.append(row)
  218. if POLLER_BREAK_ROW in row:
  219. self.lock.acquire()
  220. self.last_data = '\n'.join(self.rows)
  221. self.lock.release()
  222. self.rows = list()
  223. self.empty_rows = 0
  224. def is_started(self):
  225. return self.ident is not None
  226. def shutdown(self):
  227. self.exit = True
  228. def data(self):
  229. self.lock.acquire()
  230. data = self.last_data
  231. self.lock.release()
  232. return data
  233. def handle_attr_error(method):
  234. def on_call(*args, **kwargs):
  235. try:
  236. return method(*args, **kwargs)
  237. except AttributeError:
  238. return None
  239. return on_call
  240. def handle_value_error(method):
  241. def on_call(*args, **kwargs):
  242. try:
  243. return method(*args, **kwargs)
  244. except ValueError:
  245. return None
  246. return on_call
  247. HOST_PREFIX = os.getenv('NETDATA_HOST_PREFIX')
  248. ETC_PASSWD_PATH = '/etc/passwd'
  249. PROC_PATH = '/proc'
  250. IS_INSIDE_DOCKER = False
  251. if HOST_PREFIX:
  252. ETC_PASSWD_PATH = os.path.join(HOST_PREFIX, ETC_PASSWD_PATH[1:])
  253. PROC_PATH = os.path.join(HOST_PREFIX, PROC_PATH[1:])
  254. IS_INSIDE_DOCKER = True
  255. def read_passwd_file():
  256. data = dict()
  257. with open(ETC_PASSWD_PATH, 'r') as f:
  258. for line in f:
  259. line = line.strip()
  260. if line.startswith("#"):
  261. continue
  262. fields = line.split(":")
  263. # name, passwd, uid, gid, comment, home_dir, shell
  264. if len(fields) != 7:
  265. continue
  266. # uid, guid
  267. fields[2], fields[3] = int(fields[2]), int(fields[3])
  268. data[fields[2]] = fields
  269. return data
  270. def read_passwd_file_safe():
  271. try:
  272. if IS_INSIDE_DOCKER:
  273. return read_passwd_file()
  274. return dict((k[2], k) for k in pwd.getpwall())
  275. except (OSError, IOError):
  276. return dict()
  277. def get_username_by_pid_safe(pid, passwd_file):
  278. path = os.path.join(PROC_PATH, pid)
  279. try:
  280. uid = os.stat(path).st_uid
  281. except (OSError, IOError):
  282. return ''
  283. try:
  284. if IS_INSIDE_DOCKER:
  285. return passwd_file[uid][0]
  286. return pwd.getpwuid(uid)[0]
  287. except KeyError:
  288. return str(uid)
  289. class GPU:
  290. def __init__(self, num, root, exclude_zero_memory_users=False):
  291. self.num = num
  292. self.root = root
  293. self.exclude_zero_memory_users = exclude_zero_memory_users
  294. def id(self):
  295. return self.root.get('id')
  296. def name(self):
  297. return self.root.find('product_name').text
  298. def full_name(self):
  299. return 'gpu{0} {1}'.format(self.num, self.name())
  300. @handle_attr_error
  301. def pci_link_gen(self):
  302. return self.root.find('pci').find('pci_gpu_link_info').find('pcie_gen').find('max_link_gen').text
  303. @handle_attr_error
  304. def pci_link_width(self):
  305. info = self.root.find('pci').find('pci_gpu_link_info')
  306. return info.find('link_widths').find('max_link_width').text.split('x')[0]
  307. def pci_bw_max(self):
  308. link_gen = self.pci_link_gen()
  309. link_width = int(self.pci_link_width())
  310. if link_gen not in PCI_SPEED or link_gen not in PCI_ENCODING or not link_width:
  311. return None
  312. # Maximum PCIe Bandwidth = SPEED * WIDTH * (1 - ENCODING) - 1Gb/s.
  313. # see details https://enterprise-support.nvidia.com/s/article/understanding-pcie-configuration-for-maximum-performance
  314. # return max bandwidth in kilobytes per second (kB/s)
  315. return (PCI_SPEED[link_gen] * link_width * (1 - PCI_ENCODING[link_gen]) - 1) * 1000 * 1000 / 8
  316. @handle_attr_error
  317. def rx_util(self):
  318. return self.root.find('pci').find('rx_util').text.split()[0]
  319. @handle_attr_error
  320. def tx_util(self):
  321. return self.root.find('pci').find('tx_util').text.split()[0]
  322. @handle_attr_error
  323. def fan_speed(self):
  324. return self.root.find('fan_speed').text.split()[0]
  325. @handle_attr_error
  326. def gpu_util(self):
  327. return self.root.find('utilization').find('gpu_util').text.split()[0]
  328. @handle_attr_error
  329. def memory_util(self):
  330. return self.root.find('utilization').find('memory_util').text.split()[0]
  331. @handle_attr_error
  332. def encoder_util(self):
  333. return self.root.find('utilization').find('encoder_util').text.split()[0]
  334. @handle_attr_error
  335. def decoder_util(self):
  336. return self.root.find('utilization').find('decoder_util').text.split()[0]
  337. @handle_attr_error
  338. def fb_memory_used(self):
  339. return self.root.find('fb_memory_usage').find('used').text.split()[0]
  340. @handle_attr_error
  341. def fb_memory_free(self):
  342. return self.root.find('fb_memory_usage').find('free').text.split()[0]
  343. @handle_attr_error
  344. def bar1_memory_used(self):
  345. return self.root.find('bar1_memory_usage').find('used').text.split()[0]
  346. @handle_attr_error
  347. def bar1_memory_free(self):
  348. return self.root.find('bar1_memory_usage').find('free').text.split()[0]
  349. @handle_attr_error
  350. def temperature(self):
  351. return self.root.find('temperature').find('gpu_temp').text.split()[0]
  352. @handle_attr_error
  353. def graphics_clock(self):
  354. return self.root.find('clocks').find('graphics_clock').text.split()[0]
  355. @handle_attr_error
  356. def video_clock(self):
  357. return self.root.find('clocks').find('video_clock').text.split()[0]
  358. @handle_attr_error
  359. def sm_clock(self):
  360. return self.root.find('clocks').find('sm_clock').text.split()[0]
  361. @handle_attr_error
  362. def mem_clock(self):
  363. return self.root.find('clocks').find('mem_clock').text.split()[0]
  364. @handle_attr_error
  365. def power_readings(self):
  366. elem = self.root.find('power_readings')
  367. return elem if elem else self.root.find('gpu_power_readings')
  368. @handle_attr_error
  369. def power_state(self):
  370. return str(self.power_readings().find('power_state').text.split()[0])
  371. @handle_value_error
  372. @handle_attr_error
  373. def power_draw(self):
  374. return float(self.power_readings().find('power_draw').text.split()[0]) * 100
  375. @handle_attr_error
  376. def processes(self):
  377. processes_info = self.root.find('processes').findall('process_info')
  378. if not processes_info:
  379. return list()
  380. passwd_file = read_passwd_file_safe()
  381. processes = list()
  382. for info in processes_info:
  383. pid = info.find('pid').text
  384. processes.append({
  385. 'pid': int(pid),
  386. 'process_name': info.find('process_name').text,
  387. 'used_memory': int(info.find('used_memory').text.split()[0]),
  388. 'username': get_username_by_pid_safe(pid, passwd_file),
  389. })
  390. return processes
  391. def data(self):
  392. data = {
  393. 'rx_util': self.rx_util(),
  394. 'tx_util': self.tx_util(),
  395. 'fan_speed': self.fan_speed(),
  396. 'gpu_util': self.gpu_util(),
  397. 'memory_util': self.memory_util(),
  398. 'encoder_util': self.encoder_util(),
  399. 'decoder_util': self.decoder_util(),
  400. 'fb_memory_used': self.fb_memory_used(),
  401. 'fb_memory_free': self.fb_memory_free(),
  402. 'bar1_memory_used': self.bar1_memory_used(),
  403. 'bar1_memory_free': self.bar1_memory_free(),
  404. 'gpu_temp': self.temperature(),
  405. 'graphics_clock': self.graphics_clock(),
  406. 'video_clock': self.video_clock(),
  407. 'sm_clock': self.sm_clock(),
  408. 'mem_clock': self.mem_clock(),
  409. 'power_draw': self.power_draw(),
  410. }
  411. if self.rx_util() != NOT_AVAILABLE and self.tx_util() != NOT_AVAILABLE:
  412. pci_bw_max = self.pci_bw_max()
  413. if not pci_bw_max:
  414. data['rx_util_percent'] = 0
  415. data['tx_util_percent'] = 0
  416. else:
  417. data['rx_util_percent'] = str(int(int(self.rx_util()) * 100 / self.pci_bw_max()))
  418. data['tx_util_percent'] = str(int(int(self.tx_util()) * 100 / self.pci_bw_max()))
  419. for v in POWER_STATES:
  420. data['power_state_' + v.lower()] = 0
  421. p_state = self.power_state()
  422. if p_state:
  423. data['power_state_' + p_state.lower()] = 1
  424. processes = self.processes() or []
  425. users = set()
  426. for p in processes:
  427. data['process_mem_{0}'.format(p['pid'])] = p['used_memory']
  428. if p['username']:
  429. if self.exclude_zero_memory_users and p['used_memory'] == 0:
  430. continue
  431. users.add(p['username'])
  432. key = 'user_mem_{0}'.format(p['username'])
  433. if key in data:
  434. data[key] += p['used_memory']
  435. else:
  436. data[key] = p['used_memory']
  437. data['user_num'] = len(users)
  438. return dict(('gpu{0}_{1}'.format(self.num, k), v) for k, v in data.items())
  439. class Service(SimpleService):
  440. def __init__(self, configuration=None, name=None):
  441. super(Service, self).__init__(configuration=configuration, name=name)
  442. self.order = list()
  443. self.definitions = dict()
  444. self.loop_mode = configuration.get('loop_mode', True)
  445. poll = int(configuration.get('poll_seconds', self.get_update_every()))
  446. self.exclude_zero_memory_users = configuration.get('exclude_zero_memory_users', False)
  447. self.poller = NvidiaSMIPoller(poll)
  448. def get_data_loop_mode(self):
  449. if not self.poller.is_started():
  450. self.poller.start()
  451. if not self.poller.is_alive():
  452. self.debug('poller is off')
  453. return None
  454. return self.poller.data()
  455. def get_data_normal_mode(self):
  456. return self.poller.run_once()
  457. def get_data(self):
  458. if self.loop_mode:
  459. last_data = self.get_data_loop_mode()
  460. else:
  461. last_data = self.get_data_normal_mode()
  462. if not last_data:
  463. return None
  464. parsed = self.parse_xml(last_data)
  465. if parsed is None:
  466. return None
  467. data = dict()
  468. for idx, root in enumerate(parsed.findall('gpu')):
  469. gpu = GPU(idx, root, self.exclude_zero_memory_users)
  470. gpu_data = gpu.data()
  471. # self.debug(gpu_data)
  472. gpu_data = dict((k, v) for k, v in gpu_data.items() if is_gpu_data_value_valid(v))
  473. data.update(gpu_data)
  474. self.update_processes_mem_chart(gpu)
  475. self.update_processes_user_mem_chart(gpu)
  476. return data or None
  477. def update_processes_mem_chart(self, gpu):
  478. ps = gpu.processes()
  479. if not ps:
  480. return
  481. chart = self.charts['gpu{0}_{1}'.format(gpu.num, PROCESSES_MEM)]
  482. active_dim_ids = []
  483. for p in ps:
  484. dim_id = 'gpu{0}_process_mem_{1}'.format(gpu.num, p['pid'])
  485. active_dim_ids.append(dim_id)
  486. if dim_id not in chart:
  487. chart.add_dimension([dim_id, '{0} {1}'.format(p['pid'], p['process_name'])])
  488. for dim in chart:
  489. if dim.id not in active_dim_ids:
  490. chart.del_dimension(dim.id, hide=False)
  491. def update_processes_user_mem_chart(self, gpu):
  492. ps = gpu.processes()
  493. if not ps:
  494. return
  495. chart = self.charts['gpu{0}_{1}'.format(gpu.num, USER_MEM)]
  496. active_dim_ids = []
  497. for p in ps:
  498. if not p.get('username'):
  499. continue
  500. dim_id = 'gpu{0}_user_mem_{1}'.format(gpu.num, p['username'])
  501. active_dim_ids.append(dim_id)
  502. if dim_id not in chart:
  503. chart.add_dimension([dim_id, '{0}'.format(p['username'])])
  504. for dim in chart:
  505. if dim.id not in active_dim_ids:
  506. chart.del_dimension(dim.id, hide=False)
  507. def check(self):
  508. if not self.poller.has_smi():
  509. self.error("couldn't find '{0}' binary".format(NVIDIA_SMI))
  510. return False
  511. raw_data = self.poller.run_once()
  512. if not raw_data:
  513. self.error("failed to invoke '{0}' binary".format(NVIDIA_SMI))
  514. return False
  515. parsed = self.parse_xml(raw_data)
  516. if parsed is None:
  517. return False
  518. gpus = parsed.findall('gpu')
  519. if not gpus:
  520. return False
  521. self.create_charts(gpus)
  522. return True
  523. def parse_xml(self, data):
  524. try:
  525. return et.fromstring(data)
  526. except et.ParseError as error:
  527. self.error('xml parse failed: "{0}", error: {1}'.format(data, error))
  528. return None
  529. def create_charts(self, gpus):
  530. for idx, root in enumerate(gpus):
  531. order, charts = gpu_charts(GPU(idx, root))
  532. self.order.extend(order)
  533. self.definitions.update(charts)
  534. def is_gpu_data_value_valid(value):
  535. try:
  536. int(value)
  537. except (TypeError, ValueError):
  538. return False
  539. return True