gpustack-runtime 0.1.39.post3__py3-none-any.whl → 0.1.40.post1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. gpustack_runtime/__main__.py +7 -3
  2. gpustack_runtime/_version.py +2 -2
  3. gpustack_runtime/_version_appendix.py +1 -1
  4. gpustack_runtime/cmds/__init__.py +4 -0
  5. gpustack_runtime/cmds/deployer.py +84 -2
  6. gpustack_runtime/cmds/images.py +2 -0
  7. gpustack_runtime/deployer/__init__.py +2 -0
  8. gpustack_runtime/deployer/__types__.py +52 -28
  9. gpustack_runtime/deployer/__utils__.py +99 -112
  10. gpustack_runtime/deployer/cdi/__init__.py +81 -0
  11. gpustack_runtime/deployer/cdi/__types__.py +667 -0
  12. gpustack_runtime/deployer/cdi/thead.py +103 -0
  13. gpustack_runtime/deployer/docker.py +36 -22
  14. gpustack_runtime/deployer/kuberentes.py +8 -4
  15. gpustack_runtime/deployer/podman.py +35 -21
  16. gpustack_runtime/detector/__init__.py +62 -3
  17. gpustack_runtime/detector/__types__.py +11 -0
  18. gpustack_runtime/detector/iluvatar.py +10 -3
  19. gpustack_runtime/detector/nvidia.py +186 -97
  20. gpustack_runtime/detector/pyacl/__init__.py +9 -1
  21. gpustack_runtime/detector/pyamdgpu/__init__.py +8 -0
  22. gpustack_runtime/detector/pycuda/__init__.py +9 -1
  23. gpustack_runtime/detector/pydcmi/__init__.py +9 -2
  24. gpustack_runtime/detector/pyhgml/__init__.py +5879 -0
  25. gpustack_runtime/detector/pyhgml/libhgml.so +0 -0
  26. gpustack_runtime/detector/pyhgml/libuki.so +0 -0
  27. gpustack_runtime/detector/pyhsa/__init__.py +9 -0
  28. gpustack_runtime/detector/pyixml/__init__.py +89 -164
  29. gpustack_runtime/detector/pyrocmcore/__init__.py +42 -24
  30. gpustack_runtime/detector/pyrocmsmi/__init__.py +138 -129
  31. gpustack_runtime/detector/thead.py +733 -0
  32. gpustack_runtime/envs.py +127 -54
  33. {gpustack_runtime-0.1.39.post3.dist-info → gpustack_runtime-0.1.40.post1.dist-info}/METADATA +3 -2
  34. gpustack_runtime-0.1.40.post1.dist-info/RECORD +55 -0
  35. gpustack_runtime-0.1.39.post3.dist-info/RECORD +0 -48
  36. {gpustack_runtime-0.1.39.post3.dist-info → gpustack_runtime-0.1.40.post1.dist-info}/WHEEL +0 -0
  37. {gpustack_runtime-0.1.39.post3.dist-info → gpustack_runtime-0.1.40.post1.dist-info}/entry_points.txt +0 -0
  38. {gpustack_runtime-0.1.39.post3.dist-info → gpustack_runtime-0.1.40.post1.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,6 @@ import math
6
6
  import time
7
7
  from _ctypes import byref
8
8
  from functools import lru_cache
9
- from math import ceil
10
9
 
11
10
  import pynvml
12
11
 
@@ -78,7 +77,7 @@ class NVIDIADetector(Detector):
78
77
  def __init__(self):
79
78
  super().__init__(ManufacturerEnum.NVIDIA)
80
79
 
81
- def detect(self) -> Devices | None: # noqa: PLR0915
80
+ def detect(self) -> Devices | None:
82
81
  """
83
82
  Detect NVIDIA GPUs using pynvml.
84
83
 
@@ -140,6 +139,7 @@ class NVIDIADetector(Detector):
140
139
  dev_mig_mode, _ = pynvml.nvmlDeviceGetMigMode(dev)
141
140
 
142
141
  # With MIG disabled, treat as a single device.
142
+
143
143
  if dev_mig_mode == pynvml.NVML_DEVICE_MIG_DISABLE:
144
144
  dev_index = dev_idx
145
145
  if envs.GPUSTACK_RUNTIME_DETECT_PHYSICAL_INDEX_PRIORITY:
@@ -218,18 +218,11 @@ class NVIDIADetector(Detector):
218
218
  if dev_bdf:
219
219
  dev_appendix["bdf"] = dev_bdf
220
220
 
221
- with contextlib.suppress(pynvml.NVMLError):
222
- dev_fabric = pynvml.c_nvmlGpuFabricInfoV_t()
223
- r = pynvml.nvmlDeviceGetGpuFabricInfoV(dev, byref(dev_fabric))
224
- if r != pynvml.NVML_SUCCESS:
225
- dev_fabric = None
226
- if dev_fabric.state != pynvml.NVML_GPU_FABRIC_STATE_COMPLETED:
227
- dev_fabric = None
228
- if dev_fabric:
229
- dev_appendix["fabric_cluster_uuid"] = stringify_uuid(
230
- bytes(dev_fabric.clusterUuid),
231
- )
232
- dev_appendix["fabric_clique_id"] = dev_fabric.cliqueId
221
+ if dev_links_state := _get_links_state(dev):
222
+ dev_appendix.update(dev_links_state)
223
+
224
+ if dev_fabric_info := _get_fabric_info(dev):
225
+ dev_appendix.update(dev_fabric_info)
233
226
 
234
227
  ret.append(
235
228
  Device(
@@ -259,7 +252,7 @@ class NVIDIADetector(Detector):
259
252
  # inspired by https://github.com/NVIDIA/go-nvlib/blob/fdfe25d0ffc9d7a8c166f4639ef236da81116262/pkg/nvlib/device/mig_device.go#L61-L154.
260
253
 
261
254
  mdev_name = ""
262
- mdev_cores = 1
255
+ mdev_cores = None
263
256
  mdev_count = pynvml.nvmlDeviceGetMaxMigDeviceCount(dev)
264
257
  for mdev_idx in range(mdev_count):
265
258
  mdev = pynvml.nvmlDeviceGetMigDeviceHandleByIndex(dev, mdev_idx)
@@ -307,8 +300,6 @@ class NVIDIADetector(Detector):
307
300
  mdev_cores_util = _get_sm_util_from_gpm_metrics(dev, mdev_gi_id)
308
301
 
309
302
  if not mdev_name:
310
- mdev_attrs = pynvml.nvmlDeviceGetAttributes(mdev)
311
-
312
303
  mdev_gi = pynvml.nvmlDeviceGetGpuInstanceById(dev, mdev_gi_id)
313
304
  mdev_ci = pynvml.nvmlGpuInstanceGetComputeInstanceById(
314
305
  mdev_gi,
@@ -326,11 +317,6 @@ class NVIDIADetector(Detector):
326
317
  )
327
318
  if dev_gi_prf.id != mdev_gi_info.profileId:
328
319
  continue
329
- mdev_cores = getattr(
330
- dev_gi_prf,
331
- "multiprocessorCount",
332
- 1,
333
- )
334
320
  except pynvml.NVMLError:
335
321
  continue
336
322
 
@@ -351,31 +337,31 @@ class NVIDIADetector(Detector):
351
337
  except pynvml.NVMLError:
352
338
  continue
353
339
 
354
- gi_slices = _get_gpu_instance_slices(dev_gi_prf_id)
355
- gi_attrs = _get_gpu_instance_attrs(dev_gi_prf_id)
356
- gi_neg_attrs = _get_gpu_instance_negative_attrs(
357
- dev_gi_prf_id,
358
- )
359
- ci_slices = _get_compute_instance_slices(
340
+ ci_slice = _get_compute_instance_slice(
360
341
  dev_ci_prf_id,
361
342
  )
362
- ci_mem = _get_compute_instance_memory_in_gib(
343
+ gi_slice = _get_gpu_instance_slice(dev_gi_prf_id)
344
+ gi_mem = _get_gpu_instance_memory(
363
345
  dev_mem_info,
364
- mdev_attrs,
346
+ dev_gi_prf,
347
+ )
348
+ gi_attrs = _get_gpu_instance_attrs(dev_gi_prf_id)
349
+ gi_neg_attrs = _get_gpu_instance_negattrs(
350
+ dev_gi_prf_id,
365
351
  )
366
352
 
367
- if gi_slices == ci_slices:
368
- mdev_name = f"{gi_slices}g.{ci_mem}gb"
353
+ if ci_slice == gi_slice:
354
+ mdev_name = f"{gi_slice}g.{gi_mem}gb"
369
355
  else:
370
356
  mdev_name = (
371
- f"{ci_slices}c.{gi_slices}g.{ci_mem}gb"
357
+ f"{ci_slice}c.{gi_slice}g.{gi_mem}gb"
372
358
  )
373
359
  if gi_attrs:
374
360
  mdev_name += f"+{gi_attrs}"
375
361
  if gi_neg_attrs:
376
362
  mdev_name += f"-{gi_neg_attrs}"
377
363
 
378
- mdev_cores = ci_slices
364
+ mdev_cores = mdev_ci_prf.multiprocessorCount
379
365
 
380
366
  break
381
367
 
@@ -484,8 +470,7 @@ class NVIDIADetector(Detector):
484
470
  dev_i_handle,
485
471
  dev_j_handle,
486
472
  )
487
- # In practice, there may not be NVLINK nodes that are not interconnected.
488
- if "fabric_cluster_uuid" in dev_i.appendix:
473
+ if dev_i.appendix.get("links_state", 0) > 0:
489
474
  distance = TopologyDistanceEnum.LINK
490
475
  except pynvml.NVMLError:
491
476
  debug_log_exception(
@@ -600,6 +585,110 @@ def _get_sm_util_from_gpm_metrics(
600
585
  return None
601
586
 
602
587
 
588
+ def _extract_field_value(
589
+ field_value: pynvml.c_nvmlFieldValue_t,
590
+ ) -> int | float | None:
591
+ """
592
+ Extract the value from a NVML field value structure.
593
+
594
+ Args:
595
+ field_value:
596
+ The NVML field value structure.
597
+
598
+ Returns:
599
+ The extracted value as int, float, or None if unknown.
600
+
601
+ """
602
+ if field_value.nvmlReturn != pynvml.NVML_SUCCESS:
603
+ return None
604
+ match field_value.valueType:
605
+ case pynvml.NVML_VALUE_TYPE_DOUBLE:
606
+ return field_value.value.dVal
607
+ case pynvml.NVML_VALUE_TYPE_UNSIGNED_INT:
608
+ return field_value.value.uiVal
609
+ case pynvml.NVML_VALUE_TYPE_UNSIGNED_LONG:
610
+ return field_value.value.ulVal
611
+ case pynvml.NVML_VALUE_TYPE_UNSIGNED_LONG_LONG:
612
+ return field_value.value.ullVal
613
+ case pynvml.NVML_VALUE_TYPE_SIGNED_LONG_LONG:
614
+ return field_value.value.sllVal
615
+ case pynvml.NVML_VALUE_TYPE_SIGNED_INT:
616
+ return field_value.value.siVal
617
+ case pynvml.NVML_VALUE_TYPE_UNSIGNED_SHORT:
618
+ return field_value.value.usVal
619
+ return None
620
+
621
+
622
+ def _get_links_state(
623
+ dev: pynvml.c_nvmlDevice_t,
624
+ ) -> dict | None:
625
+ """
626
+ Get the NVLink links count and state for a device.
627
+
628
+ Args:
629
+ dev:
630
+ The NVML device handle.
631
+
632
+ Returns:
633
+ A dict includes links state or None if failed.
634
+
635
+ """
636
+ dev_links_count = 0
637
+ try:
638
+ dev_fields = pynvml.nvmlDeviceGetFieldValues(
639
+ dev,
640
+ fieldIds=[pynvml.NVML_FI_DEV_NVLINK_LINK_COUNT],
641
+ )
642
+ dev_links_count = _extract_field_value(dev_fields[0])
643
+ except pynvml.NVMLError:
644
+ debug_log_warning(logger, "Failed to get NVLink links count")
645
+ if not dev_links_count:
646
+ return None
647
+
648
+ dev_links_state = 0
649
+ try:
650
+ for link_idx in range(int(dev_links_count)):
651
+ dev_link_state = pynvml.nvmlDeviceGetNvLinkState(dev, link_idx)
652
+ if dev_link_state:
653
+ dev_links_state |= 1 << (link_idx + 1)
654
+ except pynvml.NVMLError:
655
+ debug_log_warning(logger, "Failed to get NVLink link state")
656
+
657
+ return {
658
+ "links_count": dev_links_count,
659
+ "links_state": dev_links_state,
660
+ }
661
+
662
+
663
+ def _get_fabric_info(
664
+ dev: pynvml.c_nvmlDevice_t,
665
+ ) -> dict | None:
666
+ """
667
+ Get the NVSwitch fabric information for a device.
668
+
669
+ Args:
670
+ dev:
671
+ The NVML device handle.
672
+
673
+ Returns:
674
+ A dict includes fabric info or None if failed.
675
+
676
+ """
677
+ try:
678
+ dev_fabric = pynvml.c_nvmlGpuFabricInfoV_t()
679
+ ret = pynvml.nvmlDeviceGetGpuFabricInfoV(dev, byref(dev_fabric))
680
+ if ret != pynvml.NVML_SUCCESS:
681
+ return None
682
+ if dev_fabric.state != pynvml.NVML_GPU_FABRIC_STATE_COMPLETED:
683
+ return None
684
+ return {
685
+ "fabric_cluster_uuid": stringify_uuid(bytes(dev_fabric.clusterUuid)),
686
+ "fabric_clique_id": dev_fabric.cliqueId,
687
+ }
688
+ except pynvml.NVMLError:
689
+ debug_log_warning(logger, "Failed to get NVSwitch fabric info")
690
+
691
+
603
692
  def _get_arch_family(dev_cc_t: list[int]) -> str:
604
693
  """
605
694
  Get the architecture family based on the CUDA compute capability.
@@ -636,9 +725,9 @@ def _get_arch_family(dev_cc_t: list[int]) -> str:
636
725
  return "Unknown"
637
726
 
638
727
 
639
- def _get_gpu_instance_slices(dev_gi_prf_id: int) -> int:
728
+ def _get_gpu_instance_slice(dev_gi_prf_id: int) -> int:
640
729
  """
641
- Get the number of slices for a given GPU Instance Profile ID.
730
+ Get the number of slice for a given GPU Instance Profile ID.
642
731
 
643
732
  Args:
644
733
  dev_gi_prf_id:
@@ -684,61 +773,33 @@ def _get_gpu_instance_slices(dev_gi_prf_id: int) -> int:
684
773
  raise AttributeError(msg)
685
774
 
686
775
 
687
- def _get_gpu_instance_attrs(dev_gi_prf_id: int) -> str:
776
+ def _get_gpu_instance_memory(dev_mem, dev_gi_prf) -> int:
688
777
  """
689
- Get attributes for a given GPU Instance Profile ID.
778
+ Compute the memory size of a MIG compute instance in GiB.
690
779
 
691
780
  Args:
692
- dev_gi_prf_id:
693
- The GPU Instance Profile ID.
781
+ dev_mem:
782
+ The total memory info of the parent GPU device.
783
+ dev_gi_prf:
784
+ The profile info of the GPU instance.
694
785
 
695
786
  Returns:
696
- A string representing the attributes, or an empty string if none.
697
-
698
- """
699
- match dev_gi_prf_id:
700
- case (
701
- pynvml.NVML_GPU_INSTANCE_PROFILE_1_SLICE_REV1
702
- | pynvml.NVML_GPU_INSTANCE_PROFILE_2_SLICE_REV1
703
- ):
704
- return "me"
705
- case (
706
- pynvml.NVML_GPU_INSTANCE_PROFILE_1_SLICE_ALL_ME
707
- | pynvml.NVML_GPU_INSTANCE_PROFILE_2_SLICE_ALL_ME
708
- ):
709
- return "me.all"
710
- case (
711
- pynvml.NVML_GPU_INSTANCE_PROFILE_1_SLICE_GFX
712
- | pynvml.NVML_GPU_INSTANCE_PROFILE_2_SLICE_GFX
713
- | pynvml.NVML_GPU_INSTANCE_PROFILE_4_SLICE_GFX
714
- ):
715
- return "gfx"
716
- return ""
717
-
787
+ The memory size in GiB.
718
788
 
719
- def _get_gpu_instance_negative_attrs(dev_gi_prf_id) -> str:
720
789
  """
721
- Get negative attributes for a given GPU Instance Profile ID.
722
-
723
- Args:
724
- dev_gi_prf_id:
725
- The GPU Instance Profile ID.
726
-
727
- Returns:
728
- A string representing the negative attributes, or an empty string if none.
790
+ mem = dev_gi_prf.memorySizeMB * (1 << 20) # MiB to byte
729
791
 
730
- """
731
- if dev_gi_prf_id in [
732
- pynvml.NVML_GPU_INSTANCE_PROFILE_1_SLICE_NO_ME,
733
- pynvml.NVML_GPU_INSTANCE_PROFILE_2_SLICE_NO_ME,
734
- ]:
735
- return "me"
736
- return ""
792
+ gib = round(
793
+ math.ceil(mem / dev_mem.total * 8)
794
+ / 8
795
+ * ((dev_mem.total + (1 << 30) - 1) / (1 << 30)),
796
+ )
797
+ return gib
737
798
 
738
799
 
739
- def _get_compute_instance_slices(dev_ci_prf_id: int) -> int:
800
+ def _get_compute_instance_slice(dev_ci_prf_id: int) -> int:
740
801
  """
741
- Get the number of slices for a given Compute Instance Profile ID.
802
+ Get the number of slice for a given Compute Instance Profile ID.
742
803
 
743
804
  Args:
744
805
  dev_ci_prf_id:
@@ -771,28 +832,56 @@ def _get_compute_instance_slices(dev_ci_prf_id: int) -> int:
771
832
  raise AttributeError(msg)
772
833
 
773
834
 
774
- def _get_compute_instance_memory_in_gib(dev_mem, mdev_attrs) -> int:
835
+ def _get_gpu_instance_attrs(dev_gi_prf_id: int) -> str:
775
836
  """
776
- Compute the memory size of a MIG compute instance in GiB.
837
+ Get attributes for a given GPU Instance Profile ID.
777
838
 
778
839
  Args:
779
- dev_mem:
780
- The total memory info of the parent GPU device.
781
- mdev_attrs:
782
- The attributes of the MIG device.
840
+ dev_gi_prf_id:
841
+ The GPU Instance Profile ID.
783
842
 
784
843
  Returns:
785
- The memory size in GiB.
844
+ A string representing the attributes, or an empty string if none.
786
845
 
787
846
  """
788
- gib = round(
789
- ceil(
790
- (mdev_attrs.memorySizeMB * (1 << 20)) / dev_mem.total * 8,
791
- )
792
- / 8
793
- * ((dev_mem.total + (1 << 30) - 1) / (1 << 30)),
794
- )
795
- return gib
847
+ match dev_gi_prf_id:
848
+ case (
849
+ pynvml.NVML_GPU_INSTANCE_PROFILE_1_SLICE_REV1
850
+ | pynvml.NVML_GPU_INSTANCE_PROFILE_2_SLICE_REV1
851
+ ):
852
+ return "me"
853
+ case (
854
+ pynvml.NVML_GPU_INSTANCE_PROFILE_1_SLICE_ALL_ME
855
+ | pynvml.NVML_GPU_INSTANCE_PROFILE_2_SLICE_ALL_ME
856
+ ):
857
+ return "me.all"
858
+ case (
859
+ pynvml.NVML_GPU_INSTANCE_PROFILE_1_SLICE_GFX
860
+ | pynvml.NVML_GPU_INSTANCE_PROFILE_2_SLICE_GFX
861
+ | pynvml.NVML_GPU_INSTANCE_PROFILE_4_SLICE_GFX
862
+ ):
863
+ return "gfx"
864
+ return ""
865
+
866
+
867
+ def _get_gpu_instance_negattrs(dev_gi_prf_id) -> str:
868
+ """
869
+ Get negative attributes for a given GPU Instance Profile ID.
870
+
871
+ Args:
872
+ dev_gi_prf_id:
873
+ The GPU Instance Profile ID.
874
+
875
+ Returns:
876
+ A string representing the negative attributes, or an empty string if none.
877
+
878
+ """
879
+ if dev_gi_prf_id in [
880
+ pynvml.NVML_GPU_INSTANCE_PROFILE_1_SLICE_NO_ME,
881
+ pynvml.NVML_GPU_INSTANCE_PROFILE_2_SLICE_NO_ME,
882
+ ]:
883
+ return "me"
884
+ return ""
796
885
 
797
886
 
798
887
  def _is_vgpu(dev_config: bytes) -> bool:
@@ -384,13 +384,20 @@ def convertStrBytes(func):
384
384
 
385
385
 
386
386
  def _LoadAclLibrary():
387
+ """
388
+ Load the library if it isn't loaded already.
389
+ """
387
390
  global aclLib
391
+
388
392
  if aclLib is None:
393
+ # lock to ensure only one caller loads the library
389
394
  libLoadLock.acquire()
395
+
390
396
  try:
397
+ # ensure the library still isn't loaded
391
398
  if aclLib is None:
392
399
  if sys.platform.startswith("win"):
393
- # ACL not supported on Windows
400
+ # Do not support Windows yet.
394
401
  raise ACLError(ACL_ERROR_LIBRARY_NOT_FOUND)
395
402
  # Linux path
396
403
  locs = [
@@ -419,6 +426,7 @@ def _LoadAclLibrary():
419
426
  if aclLib is None:
420
427
  raise ACLError(ACL_ERROR_LIBRARY_NOT_FOUND)
421
428
  finally:
429
+ # lock is always freed
422
430
  libLoadLock.release()
423
431
 
424
432
 
@@ -212,10 +212,17 @@ class c_amdgpu_gpu_info(_PrintableStructure):
212
212
 
213
213
 
214
214
  def _LoadAMDGPULibrary():
215
+ """
216
+ Load the library if it isn't loaded already.
217
+ """
215
218
  global amdgpuLib
219
+
216
220
  if amdgpuLib is None:
221
+ # lock to ensure only one caller loads the library
217
222
  libLoadLock.acquire()
223
+
218
224
  try:
225
+ # ensure the library still isn't loaded
219
226
  if amdgpuLib is None:
220
227
  if sys.platform.startswith("win"):
221
228
  # Do not support Windows yet.
@@ -235,6 +242,7 @@ def _LoadAMDGPULibrary():
235
242
  if amdgpuLib is None:
236
243
  raise AMDGPUError(AMDGPU_ERROR_LIBRARY_NOT_FOUND)
237
244
  finally:
245
+ # lock is always freed
238
246
  libLoadLock.release()
239
247
 
240
248
 
@@ -1,10 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- import contextlib
4
3
  import string
5
4
  import sys
6
5
  import threading
7
6
  from ctypes import *
7
+ from functools import wraps
8
8
  from typing import ClassVar
9
9
 
10
10
  ## C Type mappings ##
@@ -390,10 +390,17 @@ def convertStrBytes(func):
390
390
 
391
391
 
392
392
  def _LoadCudaLibrary():
393
+ """
394
+ Load the library if it isn't loaded already.
395
+ """
393
396
  global cudaLib
397
+
394
398
  if cudaLib is None:
399
+ # lock to ensure only one caller loads the library
395
400
  libLoadLock.acquire()
401
+
396
402
  try:
403
+ # ensure the library still isn't loaded
397
404
  if cudaLib is None:
398
405
  if sys.platform.startswith("win"):
399
406
  # Do not support Windows yet.
@@ -411,6 +418,7 @@ def _LoadCudaLibrary():
411
418
  if cudaLib is None:
412
419
  raise CUDAError(CUDA_ERROR_LIBRARY_NOT_FOUND)
413
420
  finally:
421
+ # lock is always freed
414
422
  libLoadLock.release()
415
423
 
416
424
 
@@ -733,14 +733,20 @@ def convertStrBytes(func):
733
733
 
734
734
 
735
735
  def _LoadDcmiLibrary():
736
+ """
737
+ Load the library if it isn't loaded already.
738
+ """
736
739
  global dcmiLib
740
+
737
741
  if dcmiLib is None:
742
+ # lock to ensure only one caller loads the library
738
743
  libLoadLock.acquire()
744
+
739
745
  try:
746
+ # ensure the library still isn't loaded
740
747
  if dcmiLib is None:
741
748
  if sys.platform.startswith("win"):
742
- # DCMI is typically used on Linux, but for completeness,
743
- # Windows support would require different path handling.
749
+ # Do not support Windows yet.
744
750
  raise DCMIError(DCMI_ERROR_LIBRARY_NOT_FOUND)
745
751
  # Linux path
746
752
  locs = [
@@ -757,6 +763,7 @@ def _LoadDcmiLibrary():
757
763
  if dcmiLib is None:
758
764
  raise DCMIError(DCMI_ERROR_LIBRARY_NOT_FOUND)
759
765
  finally:
766
+ # lock is always freed
760
767
  libLoadLock.release()
761
768
 
762
769