Repository: wookayin/gpustat

Issue:
Show fan speed
Command `nvidia-smi` shows also fan speed:

```bash
$ nvidia-smi
Wed Apr  3 14:09:10 2019
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 396.37                 Driver Version: 396.37                    |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  On   | 00000000:03:00.0  On |                  N/A |
| 30%   42C    P8    16W / 250W |     53MiB / 11177MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  GeForce GTX 108...  On   | 00000000:04:00.0 Off |                  N/A |
| 31%   43C    P8    16W / 250W |      2MiB / 11178MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   2  GeForce GTX 108...  On   | 00000000:81:00.0 Off |                  N/A |
| 51%   68C    P2    76W / 250W |  10781MiB / 11178MiB |     17%      Default |
+-------------------------------+----------------------+----------------------+
|   3  GeForce GTX 108...  On   | 00000000:82:00.0 Off |                  N/A |
| 29%   34C    P8    16W / 250W |      2MiB / 11178MiB |      0%      Default |
```

Could gpustat show this information too? 

Discussion:
bethune-bryant: @wookayin Does this look good to you?
wookayin: @bethune-bryant Thank for your contribution. Can you fix the travis CI failure?

Patch:
diff --git a/README.md b/README.md
index 4b9d24c..33bbad4 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ Options:
 * `-u`, `--show-user`  : Display username of the process owner
 * `-c`, `--show-cmd`   : Display the process name
 * `-p`, `--show-pid`   : Display PID of the process
+* `-F`, `--show-fan`   : Display GPU fan speed
 * `-P`, `--show-power` : Display GPU power usage and/or limit (`draw` or `draw,limit`)
 * `--watch`, `-i`, `--interval`   : Run in watch mode (equivalent to `watch gpustat`) if given. Denotes interval between updates. ([#41][gh-issue-41])
 * `--json`             : JSON Output (Experimental, [#10][gh-issue-10])
diff --git a/gpustat/__main__.py b/gpustat/__main__.py
index dab7954..8de332f 100644
--- a/gpustat/__main__.py
+++ b/gpustat/__main__.py
@@ -79,6 +79,8 @@ def main(*argv):
                         help='Display username of running process')
     parser.add_argument('-p', '--show-pid', action='store_true',
                         help='Display PID of running process')
+    parser.add_argument('-F', '--show-fan', action='store_true',
+                        help='Display GPU fan speed')
     parser.add_argument('--json', action='store_true', default=False,
                         help='Print all the information in JSON format')
     parser.add_argument('-v', '--version', action='version',
diff --git a/gpustat/core.py b/gpustat/core.py
index 85c85cf..dad85a3 100644
--- a/gpustat/core.py
+++ b/gpustat/core.py
@@ -107,6 +107,15 @@ class GPUStat(object):
         v = self.entry['temperature.gpu']
         return int(v) if v is not None else None
 
+    @property
+    def fan(self):
+        """
+        Returns the fan percentage of GPU as an integer,
+        or None if the information is not available.
+        """
+        v = self.entry['fan.speed']
+        return int(v) if v is not None else None
+
     @property
     def utilization(self):
         """
@@ -147,6 +156,7 @@ class GPUStat(object):
                  show_user=False,
                  show_pid=False,
                  show_power=None,
+                 show_fan=None,
                  gpuname_width=16,
                  term=Terminal(),
                  ):
@@ -165,6 +175,8 @@ class GPUStat(object):
         colors['CName'] = term.blue
         colors['CTemp'] = _conditional(lambda: self.temperature < 50,
                                        term.red, term.bold_red)
+        colors['FSpeed'] = _conditional(lambda: self.fan < 50,
+                                        term.yellow, term.bold_yellow)
         colors['CMemU'] = term.bold_yellow
         colors['CMemT'] = term.yellow
         colors['CMemP'] = term.yellow
@@ -189,8 +201,12 @@ class GPUStat(object):
         # temperature and utilization
         reps = "%(C1)s[{entry[index]}]%(C0)s " \
             "%(CName)s{entry[name]:{gpuname_width}}%(C0)s |" \
-            "%(CTemp)s{entry[temperature.gpu]:>3}'C%(C0)s, " \
-            "%(CUtil)s{entry[utilization.gpu]:>3} %%%(C0)s"
+            "%(CTemp)s{entry[temperature.gpu]:>3}'C%(C0)s, "
+
+        if show_fan:
+            reps += "%(FSpeed)s{entry[fan.speed]:>3} %%%(C0)s, "
+
+        reps += "%(CUtil)s{entry[utilization.gpu]:>3} %%%(C0)s"
 
         if show_power:
             reps += ",  %(CPowU)s{entry[power.draw]:>3}%(C0)s "
@@ -300,6 +316,11 @@ class GPUStatCollection(object):
             except N.NVMLError:
                 temperature = None  # Not supported
 
+            try:
+                fan_speed = N.nvmlDeviceGetFanSpeed(handle)
+            except N.NVMLError:
+                fan_speed = None  # Not supported
+
             try:
                 memory = N.nvmlDeviceGetMemoryInfo(handle)  # in Bytes
             except N.NVMLError:
@@ -354,6 +375,7 @@ class GPUStatCollection(object):
                 'uuid': uuid,
                 'name': name,
                 'temperature.gpu': temperature,
+                'fan.speed': fan_speed,
                 'utilization.gpu': utilization.gpu if utilization else None,
                 'power.draw': power // 1000 if power is not None else None,
                 'enforced.power.limit': power_limit // 1000
@@ -403,7 +425,7 @@ class GPUStatCollection(object):
 
     def print_formatted(self, fp=sys.stdout, force_color=False, no_color=False,
                         show_cmd=False, show_user=False, show_pid=False,
-                        show_power=None, gpuname_width=16,
+                        show_power=None, show_fan=None, gpuname_width=16,
                         show_header=True,
                         eol_char=os.linesep,
                         **kwargs
@@ -453,6 +475,7 @@ class GPUStatCollection(object):
                        show_user=show_user,
                        show_pid=show_pid,
                        show_power=show_power,
+                       show_fan=show_fan,
                        gpuname_width=gpuname_width,
                        term=t_color)
             fp.write(eol_char)


Test patch:
diff --git a/gpustat/test_gpustat.py b/gpustat/test_gpustat.py
index d41dd6b..088b6bd 100644
--- a/gpustat/test_gpustat.py
+++ b/gpustat/test_gpustat.py
@@ -81,6 +81,12 @@ def _configure_mock(N, Process,
         mock_handles[2]: 71,
     }.get(handle, RuntimeError))
 
+    N.nvmlDeviceGetFanSpeed = _raise_ex(lambda handle: {
+        mock_handles[0]: 16,
+        mock_handles[1]: 53,
+        mock_handles[2]: 100,
+    }.get(handle, RuntimeError))
+
     N.nvmlDeviceGetPowerUsage = _raise_ex(lambda handle: {
         mock_handles[0]: 125000,
         mock_handles[1]: N.NVMLError_NotSupported(),  # Not Supported
@@ -154,9 +160,9 @@ MOCK_EXPECTED_OUTPUT_DEFAULT = """\
 """  # noqa: E501
 
 MOCK_EXPECTED_OUTPUT_FULL = """\
-[0] GeForce GTX TITAN 0 | 80'C,  76 %,  125 / 250 W |  8000 / 12287 MB | user1:python/48448(4000M) user2:python/153223(4000M)
-[1] GeForce GTX TITAN 1 | 36'C,   0 %,   ?? / 250 W |  9000 / 12189 MB | user1:torch/192453(3000M) user3:caffe/194826(6000M)
-[2] GeForce GTX TITAN 2 | 71'C,  ?? %,  250 /  ?? W |     0 / 12189 MB | (Not Supported)
+[0] GeForce GTX TITAN 0 | 80'C,  16 %,  76 %,  125 / 250 W |  8000 / 12287 MB | user1:python/48448(4000M) user2:python/153223(4000M)
+[1] GeForce GTX TITAN 1 | 36'C,  53 %,   0 %,   ?? / 250 W |  9000 / 12189 MB | user1:torch/192453(3000M) user3:caffe/194826(6000M)
+[2] GeForce GTX TITAN 2 | 71'C, 100 %,  ?? %,  250 /  ?? W |     0 / 12189 MB | (Not Supported)
 """  # noqa: E501
 
 
@@ -195,7 +201,7 @@ class TestGPUStat(unittest.TestCase):
         fp = StringIO()
         gpustats.print_formatted(
             fp=fp, no_color=False, show_user=True,
-            show_cmd=True, show_pid=True, show_power=True
+            show_cmd=True, show_pid=True, show_power=True, show_fan=True
         )
 
         result = fp.getvalue()