gpustack-runtime 0.2.6__tar.gz → 0.2.7__tar.gz

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 (162) hide show
  1. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/PKG-INFO +2 -2
  2. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/_version.py +2 -2
  3. gpustack_runtime-0.2.7/gpustack_runtime/_version_appendix.py +1 -0
  4. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/__types__.py +294 -1
  5. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/amd.py +13 -19
  6. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/ascend.py +157 -34
  7. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/cambricon.py +19 -3
  8. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/iluvatar.py +7 -5
  9. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/nvidia.py +14 -9
  10. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pydcmi/__init__.py +11 -0
  11. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/thead.py +8 -5
  12. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/envs.py +30 -6
  13. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/pyproject.toml +7 -1
  14. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/specs/2026-08-14-detector-alignment-and-workload-exit-status.md +2 -2
  15. gpustack_runtime-0.2.7/specs/2026-10-30-detector-health-alignment-and-identity-cache.md +479 -0
  16. gpustack_runtime-0.2.7/tests/gpustack_runtime/detector/conftest.py +19 -0
  17. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_amd.py +53 -14
  18. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_ascend.py +312 -11
  19. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_cambricon.py +112 -4
  20. gpustack_runtime-0.2.7/tests/gpustack_runtime/detector/test_detector_cache.py +544 -0
  21. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_iluvatar.py +20 -8
  22. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_nvidia.py +119 -18
  23. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_pydmi.py +73 -1
  24. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_thead.py +59 -24
  25. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/uv.lock +42 -19
  26. gpustack_runtime-0.2.6/gpustack_runtime/_version_appendix.py +0 -1
  27. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/.agents/skills/runtime-release/SKILL.md +0 -0
  28. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/.codespelldict +0 -0
  29. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/.codespellrc +0 -0
  30. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/.dockerignore +0 -0
  31. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/.gitattributes +0 -0
  32. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/.gitignore +0 -0
  33. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/.pre-commit-config.yaml +0 -0
  34. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/.python-version +0 -0
  35. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/ADOPTERS.md +0 -0
  36. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/AGENTS.md +0 -0
  37. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/DCO +0 -0
  38. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/LICENSE +0 -0
  39. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/Makefile +0 -0
  40. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/README.md +0 -0
  41. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/deploy/manifests/docker-compose.yaml +0 -0
  42. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/deploy/manifests/kubernetes.yaml +0 -0
  43. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/docs/index.md +0 -0
  44. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/docs/modules/gpustack_runtime.deployer.md +0 -0
  45. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/docs/modules/gpustack_runtime.detector.md +0 -0
  46. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/docs/modules/gpustack_runtime.md +0 -0
  47. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/__init__.py +0 -0
  48. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/__main__.py +0 -0
  49. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/_version.pyi +0 -0
  50. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/cmds/__init__.py +0 -0
  51. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/cmds/__types__.py +0 -0
  52. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/cmds/deployer.py +0 -0
  53. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/cmds/detector.py +0 -0
  54. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/cmds/images.py +0 -0
  55. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/__init__.py +0 -0
  56. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/__patches__.py +0 -0
  57. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/__types__.py +0 -0
  58. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/__utils__.py +0 -0
  59. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/cdi/__init__.py +0 -0
  60. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/cdi/__types__.py +0 -0
  61. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/cdi/__utils__.py +0 -0
  62. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/cdi/amd.py +0 -0
  63. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/cdi/ascend.py +0 -0
  64. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/cdi/hygon.py +0 -0
  65. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/cdi/iluvatar.py +0 -0
  66. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/cdi/metax.py +0 -0
  67. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/cdi/thead.py +0 -0
  68. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/docker.py +0 -0
  69. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/k8s/devicemanager/__init__.py +0 -0
  70. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/kubernetes.py +0 -0
  71. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/deployer/podman.py +0 -0
  72. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/__init__.py +0 -0
  73. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/__utils__.py +0 -0
  74. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/hygon.py +0 -0
  75. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/metax.py +0 -0
  76. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/mthreads.py +0 -0
  77. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pyamdgpu/__init__.py +0 -0
  78. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pyamdsmi/__init__.py +0 -0
  79. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pycndev/__init__.py +0 -0
  80. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pydmi/__init__.py +0 -0
  81. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pyhgml/__init__.py +0 -0
  82. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pyhgml/libhgml.so +0 -0
  83. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pyhgml/libuki.so +0 -0
  84. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pyhsa/__init__.py +0 -0
  85. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pyixml/__init__.py +0 -0
  86. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pymtml/__init__.py +0 -0
  87. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pymxsml/__init__.py +0 -0
  88. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pynvml/__init__.py +0 -0
  89. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/detector/pyrocmsmi/__init__.py +0 -0
  90. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/gpustack_runtime/logging.py +0 -0
  91. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/hatch.toml +0 -0
  92. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/mkdocs.yml +0 -0
  93. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/pack/Dockerfile +0 -0
  94. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/pack/Dockerfile.dummy +0 -0
  95. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/pytest.ini +0 -0
  96. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/ruff.toml +0 -0
  97. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/fixtures/__init__.py +0 -0
  98. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/fixtures/test_compare_versions.json +0 -0
  99. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/fixtures/test_correct_runner_image.json +0 -0
  100. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json.json +0 -0
  101. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_multiple_jsons.json +0 -0
  102. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_multiple_yamls.yaml +0 -0
  103. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_single_json.json +0 -0
  104. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_single_yaml.yaml +0 -0
  105. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/fixtures/test_nginx_entrypoint.sh +0 -0
  106. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_ascend_ranktable.py +0 -0
  107. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_docker_status.py +0 -0
  108. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_kubernetes_status.py +0 -0
  109. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_logs_options.py +0 -0
  110. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_podman_status.py +0 -0
  111. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_privileged.py +0 -0
  112. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_resource_injection_policy.py +0 -0
  113. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_runtime_class.py +0 -0
  114. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_termination_grace_period.py +0 -0
  115. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_utils.py +0 -0
  116. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_visible_devices_ordering.py +0 -0
  117. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_workload_resource_injection_policy.py +0 -0
  118. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/deployer/test_workload_status.py +0 -0
  119. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/fixtures/__init__.py +0 -0
  120. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/README.md +0 -0
  121. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi300x.json +0 -0
  122. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi308x.json +0 -0
  123. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_amd_rx7800xt.json +0 -0
  124. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_ascend_310p3.json +0 -0
  125. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_ascend_910b2.json +0 -0
  126. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_hygon_k100ai.json +0 -0
  127. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_metax_c500.json +0 -0
  128. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_gb10.json +0 -0
  129. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100.json +0 -0
  130. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100_mig.json +0 -0
  131. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h200.json +0 -0
  132. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4080super.json +0 -0
  133. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090_48g.json +0 -0
  134. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090d.json +0 -0
  135. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx5090d.json +0 -0
  136. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/detect_output_thead_ppu.json +0 -0
  137. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi300x.json +0 -0
  138. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi308x.json +0 -0
  139. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_amd_rx7800xt.json +0 -0
  140. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_ascend_310p3.json +0 -0
  141. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_ascend_910b2.json +0 -0
  142. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_hygon_k100ai.json +0 -0
  143. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_metax_c500.json +0 -0
  144. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_mthreads_s5000.json +0 -0
  145. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100.json +0 -0
  146. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100_mig.json +0 -0
  147. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h200.json +0 -0
  148. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4080super.json +0 -0
  149. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090_48g.json +0 -0
  150. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090d.json +0 -0
  151. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx5090d.json +0 -0
  152. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/samples/topology_output_thead_ppu.json +0 -0
  153. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_detector_cli.py +0 -0
  154. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_detector_types.py +0 -0
  155. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_detector_utils.py +0 -0
  156. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_hygon.py +0 -0
  157. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_metax.py +0 -0
  158. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_mig_devices.py +0 -0
  159. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_mthreads.py +0 -0
  160. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_pycndev.py +0 -0
  161. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/tests/gpustack_runtime/detector/test_samples.py +0 -0
  162. {gpustack_runtime-0.2.6 → gpustack_runtime-0.2.7}/uv.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gpustack-runtime
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: GPUStack Runtime is library for detecting GPU resources and launching GPU workloads.
5
5
  Project-URL: Homepage, https://github.com/gpustack/runtime
6
6
  Project-URL: Bug Tracker, https://github.com/gpustack/gpustack/issues
@@ -16,7 +16,7 @@ Requires-Python: >=3.10
16
16
  Requires-Dist: argcomplete>=3.6.3
17
17
  Requires-Dist: cachetools>=5.5.2
18
18
  Requires-Dist: docker>=7.1.0
19
- Requires-Dist: gpustack-runner>=0.1.31
19
+ Requires-Dist: gpustack-runner>=0.1.32.post2
20
20
  Requires-Dist: kubernetes>=33.1.0
21
21
  Requires-Dist: mthreads-ml-py>=2.2.10
22
22
  Requires-Dist: nvidia-ml-py>=13.580.65
@@ -27,8 +27,8 @@ version_tuple: VERSION_TUPLE
27
27
  __commit_id__: COMMIT_ID
28
28
  commit_id: COMMIT_ID
29
29
 
30
- __version__ = version = '0.2.6'
31
- __version_tuple__ = version_tuple = (0, 2, 6)
30
+ __version__ = version = '0.2.7'
31
+ __version_tuple__ = version_tuple = (0, 2, 7)
32
32
  try:
33
33
  from ._version_appendix import git_commit
34
34
  __commit_id__ = commit_id = git_commit
@@ -0,0 +1 @@
1
+ git_commit = "489860c"
@@ -1,6 +1,9 @@
1
1
  from __future__ import annotations as __future_annotations__
2
2
 
3
+ import copy
3
4
  import logging
5
+ import threading
6
+ import time
4
7
  from abc import ABC, abstractmethod
5
8
  from dataclasses import dataclass
6
9
  from enum import Enum
@@ -9,10 +12,17 @@ from typing import Any
9
12
 
10
13
  from dataclasses_json import dataclass_json
11
14
 
15
+ from .. import envs
12
16
  from ..logging import debug_log_exception, debug_log_warning
13
17
 
14
18
  logger = logging.getLogger(__name__)
15
19
 
20
+ _monotonic = time.monotonic
21
+ """
22
+ The clock the identity cache's time to live is measured on. A module level
23
+ indirection, so tests can inject a clock of their own.
24
+ """
25
+
16
26
 
17
27
  class ManufacturerEnum(str, Enum):
18
28
  """
@@ -606,6 +616,285 @@ def _usage_fields_to_merge(usage: Device) -> tuple[str, ...]:
606
616
  return tuple(field for field in _DEVICE_USAGE_FIELDS if field != "memory_status")
607
617
 
608
618
 
619
+ def _clear_health_verdict(devices: Devices) -> None:
620
+ """
621
+ Drop the health verdict of the given devices, in place.
622
+
623
+ A verdict read when the devices were detected says nothing about them
624
+ now, and the refresh that could have confirmed it failed. So the devices
625
+ are served as inventory, with no verdict, rather than with one that went
626
+ stale.
627
+
628
+ Args:
629
+ devices:
630
+ The devices to drop the verdict of.
631
+
632
+ """
633
+ for dev in devices:
634
+ dev.memory_status = DeviceMemoryStatusEnum.UNKNOWN
635
+ # A MIG device is an entry of the card's appendix, and carries its own
636
+ # verdict, which the CLI reports as well.
637
+ for mig_dev in (dev.appendix or {}).get("mig_devices") or []:
638
+ mig_dev["memory_status"] = DeviceMemoryStatusEnum.UNKNOWN
639
+
640
+
641
+ @dataclass
642
+ class _IdentityCacheEntry:
643
+ """
644
+ A cached device identity and the moment it expires.
645
+ """
646
+
647
+ devices: Devices
648
+ """
649
+ The detected identity, handed out as a copy.
650
+ """
651
+
652
+ expires_at: float
653
+ """
654
+ The clock reading after which the identity is stale.
655
+ """
656
+
657
+
658
+ class _RefreshFlight:
659
+ """
660
+ One identity refresh of a manufacturer.
661
+
662
+ A flight carries its own completion, so a caller that joined it takes
663
+ that flight's outcome, whatever a later flight does.
664
+ """
665
+
666
+ def __init__(self, stale: Devices | None = None):
667
+ self.done = threading.Event()
668
+ self.devices: Devices | None = None
669
+ self.error: BaseException | None = None
670
+ self.stale = stale
671
+ """
672
+ The last good identity when the flight started: the fallback a failed
673
+ flight owes its callers.
674
+ """
675
+
676
+ def complete(self, devices: Devices | None, error: BaseException | None) -> None:
677
+ """
678
+ Complete the flight with the devices it detected or the error it
679
+ raised.
680
+
681
+ Args:
682
+ devices:
683
+ The devices the flight detected, empty when it detected none.
684
+ error:
685
+ The exception the flight raised, None when it succeeded.
686
+
687
+ """
688
+ self.devices = devices
689
+ self.error = error
690
+ self.done.set()
691
+
692
+ def outcome(self) -> Devices | None:
693
+ """
694
+ Return the devices the flight detected, or the last good identity
695
+ when the flight failed.
696
+
697
+ The last good identity is served without its health verdict: the
698
+ verdict was read when the identity was detected, and the refresh that
699
+ could have confirmed it failed.
700
+
701
+ Returns:
702
+ The devices to serve, as a copy.
703
+
704
+ Raises:
705
+ The exception the flight raised, when no identity is cached to
706
+ fall back on.
707
+
708
+ """
709
+ if self.error is not None:
710
+ if self.stale is None:
711
+ raise self.error
712
+ # The last good identity is better than nothing, and the next call
713
+ # retries the detection.
714
+ devices = copy.deepcopy(self.stale)
715
+ _clear_health_verdict(devices)
716
+ return devices
717
+ return copy.deepcopy(self.devices)
718
+
719
+
720
+ class _VendorIdentityState:
721
+ """
722
+ The identity cache state of one manufacturer.
723
+
724
+ The condition guards the entry and the live flight. A refresh runs
725
+ outside the lock, so a wedged vendor delays only the callers waiting for
726
+ that vendor.
727
+ """
728
+
729
+ def __init__(self):
730
+ self.condition = threading.Condition()
731
+ self.entry: _IdentityCacheEntry | None = None
732
+ self.flight: _RefreshFlight | None = None
733
+ """
734
+ The refresh running now, if any. A caller that finds one joins it
735
+ rather than starting a second detection of the same vendor.
736
+ """
737
+
738
+
739
+ class _IdentityCache:
740
+ """
741
+ The process wide cache of detected device identities.
742
+
743
+ One state per manufacturer, so a slow refresh of one vendor delays only
744
+ the callers waiting for that vendor.
745
+ """
746
+
747
+ def __init__(self):
748
+ self._lock = threading.Lock()
749
+ self._states: dict[ManufacturerEnum, _VendorIdentityState] = {}
750
+
751
+ def state(self, manufacturer: ManufacturerEnum) -> _VendorIdentityState:
752
+ """
753
+ Return the cache state of the given manufacturer.
754
+
755
+ Args:
756
+ manufacturer:
757
+ The manufacturer to take the state of.
758
+
759
+ Returns:
760
+ The state of the manufacturer.
761
+
762
+ """
763
+ with self._lock:
764
+ state = self._states.get(manufacturer)
765
+ if state is None:
766
+ state = _VendorIdentityState()
767
+ self._states[manufacturer] = state
768
+ return state
769
+
770
+ def reset(self) -> None:
771
+ """
772
+ Drop every cached identity.
773
+ """
774
+ with self._lock:
775
+ self._states.clear()
776
+
777
+ def identity(self, detector: Detector, ttl: float) -> Devices | None:
778
+ """
779
+ Return the identity of the given detector, from the cache when it is
780
+ still fresh.
781
+
782
+ The identity is handed out as a copy: the usage merge writes into the
783
+ devices it is given, so a shared object would let one caller
784
+ contaminate the cache.
785
+
786
+ A fresh identity is served with the health verdict it was detected
787
+ with. A stale one is served without any verdict: the refresh that
788
+ could have confirmed it failed, so the last good identity stands for
789
+ the inventory only.
790
+
791
+ Args:
792
+ detector:
793
+ The detector to take the identity of.
794
+ ttl:
795
+ The time to live of a cached identity, in seconds.
796
+ Zero disables the cache.
797
+
798
+ Returns:
799
+ The devices carrying the identity, or None if there is none.
800
+
801
+ Raises:
802
+ The exception the detection raised, when the cache holds no
803
+ identity to fall back on. An interruption propagates to the caller
804
+ that asked for the detection, and the waiters of the same refresh
805
+ get its outcome.
806
+
807
+ """
808
+ if ttl <= 0:
809
+ # The cache is disabled: the identity is detected on every call.
810
+ return detector.detect_info()
811
+
812
+ state = self.state(detector.manufacturer)
813
+ with state.condition:
814
+ entry = state.entry
815
+ if entry is not None and _monotonic() < entry.expires_at:
816
+ return copy.deepcopy(entry.devices)
817
+ flight = state.flight
818
+ if flight is not None:
819
+ # A refresh is running: join it, so the callers of one vendor
820
+ # share one detection. The flight carries its own completion,
821
+ # so a later flight cannot postpone this caller's outcome.
822
+ state.condition.wait_for(flight.done.is_set)
823
+ return flight.outcome()
824
+ # No refresh is running: start one. The last good identity is the
825
+ # fallback the flight owes its callers when it fails.
826
+ flight = _RefreshFlight(
827
+ stale=entry.devices if entry is not None else None,
828
+ )
829
+ state.flight = flight
830
+
831
+ # The detection runs outside the lock: a wedged vendor must not hold
832
+ # the cache for the other vendors.
833
+ devices: Devices | None = None
834
+ error: BaseException | None = None
835
+ try:
836
+ devices = detector.detect_info()
837
+ except Exception as e:
838
+ debug_log_exception(
839
+ logger,
840
+ "Failed to detect %s devices identity, keeping the last known one",
841
+ detector.manufacturer,
842
+ )
843
+ error = e
844
+ except BaseException as e:
845
+ # An interruption settles the flight as well: a flight left
846
+ # pending blocks every later caller of the vendor forever. The
847
+ # interruption keeps propagating to the thread that asked for the
848
+ # detection, while the waiters get the flight's outcome.
849
+ debug_log_exception(
850
+ logger,
851
+ "Interrupted while detecting %s devices identity",
852
+ detector.manufacturer,
853
+ )
854
+ error = e
855
+ raise
856
+ finally:
857
+ with state.condition:
858
+ if error is None and devices:
859
+ # Only a non empty identity is cached: one enumeration glitch
860
+ # must not pin an empty inventory for the time to live.
861
+ state.entry = _IdentityCacheEntry(
862
+ devices=devices,
863
+ expires_at=_monotonic() + ttl,
864
+ )
865
+ flight.complete(devices, error)
866
+ state.flight = None
867
+ state.condition.notify_all()
868
+ return flight.outcome()
869
+
870
+
871
+ _IDENTITY_CACHE = _IdentityCache()
872
+ """
873
+ The process wide cache of detected device identities.
874
+ """
875
+
876
+
877
+ def _identity_cache_ttl() -> float:
878
+ """
879
+ Return the time to live of a cached device identity, in seconds.
880
+
881
+ Zero disables the cache. The environment variable is read once per
882
+ process, so it must be set before the first detection.
883
+
884
+ Returns:
885
+ The time to live, in seconds.
886
+
887
+ """
888
+ return float(envs.GPUSTACK_RUNTIME_DETECT_INFO_CACHE_TTL)
889
+
890
+
891
+ def _reset_identity_cache() -> None:
892
+ """
893
+ Drop every cached device identity.
894
+ """
895
+ _IDENTITY_CACHE.reset()
896
+
897
+
609
898
  class Detector(ABC):
610
899
  """
611
900
  Base class for all detectors.
@@ -677,6 +966,10 @@ class Detector(ABC):
677
966
  """
678
967
  Detect devices and return a list of Device objects.
679
968
 
969
+ The identity of the devices is cached process wide, per manufacturer,
970
+ so a repeated collection pays only the usage query. The usage is read
971
+ on every call, and so is the health it carries.
972
+
680
973
  Args:
681
974
  usage:
682
975
  Whether to fetch the devices' usage as well.
@@ -685,7 +978,7 @@ class Detector(ABC):
685
978
  A list of detected Device objects, or None if detection fails.
686
979
 
687
980
  """
688
- devices = self.detect_info()
981
+ devices = _IDENTITY_CACHE.identity(self, _identity_cache_ttl())
689
982
  if usage and devices:
690
983
  try:
691
984
  self.detect_usage(devices)
@@ -502,6 +502,9 @@ def _get_memory_status(dev, dev_idx: int) -> DeviceMemoryStatusEnum:
502
502
  Both queries produce it, mirroring the operator, which reports `Unhealthy`
503
503
  from `DetectAccelerator` and `MonitorAccelerator` alike.
504
504
 
505
+ A failed read yields no verdict, as the operator's counter read does: only
506
+ a readable value may condemn a card.
507
+
505
508
  Args:
506
509
  dev:
507
510
  The AMD SMI device handle.
@@ -521,14 +524,13 @@ def _get_memory_status(dev, dev_idx: int) -> DeviceMemoryStatusEnum:
521
524
  pyamdsmi.AmdSmiGpuBlock.UMC,
522
525
  )
523
526
  except pyamdsmi.AmdSmiException as e:
524
- # Fail closed: a genuine driver error marks the device unhealthy, while
525
- # a card without the counter cannot be judged. The stub binding raises
526
- # a codeless error when amdsmi is absent, in which case ROCm SMI is
527
- # the fallback, as it is for the memory read.
527
+ # Fail open: a genuine driver error yields no verdict, as the
528
+ # operator's counter read does. The stub binding raises a codeless
529
+ # error when amdsmi is absent, in which case ROCm SMI is the
530
+ # fallback, as it is for the memory read.
528
531
  err_code = getattr(e, "err_code", None)
529
532
  if err_code is not None:
530
- if err_code != getattr(pyamdsmi, "AMDSMI_STATUS_NOT_SUPPORTED", None):
531
- return DeviceMemoryStatusEnum.UNHEALTHY
533
+ debug_log_warning(logger, "Failed to read the UMC ECC error count")
532
534
  return DeviceMemoryStatusEnum.HEALTHY
533
535
  else:
534
536
  if dev_ecc_count.get("uncorrectable_count", 0) > 0:
@@ -539,19 +541,11 @@ def _get_memory_status(dev, dev_idx: int) -> DeviceMemoryStatusEnum:
539
541
  dev_ecc_count = pyrocmsmi.rsmi_dev_ecc_count_get(dev_idx)
540
542
  if dev_ecc_count.uncorrectable_err > 0:
541
543
  return DeviceMemoryStatusEnum.UNHEALTHY
542
- except pyrocmsmi.ROCMSMIError as e:
543
- # rsmi_status_t comes from the ROCm-installed rsmiBindings, so it is
544
- # looked up late: without ROCm the module has no such attribute.
545
- rsmi_not_supported = getattr(
546
- getattr(pyrocmsmi, "rsmi_status_t", None),
547
- "RSMI_STATUS_NOT_SUPPORTED",
548
- None,
549
- )
550
- if e.value not in (
551
- pyrocmsmi.ROCMSMI_ERROR_FUNCTION_NOT_FOUND,
552
- rsmi_not_supported,
553
- ):
554
- return DeviceMemoryStatusEnum.UNHEALTHY
544
+ except pyrocmsmi.ROCMSMIError:
545
+ # Fail open: a terminal error inside the fallback yields no verdict
546
+ # either, so the card stays unjudged.
547
+ debug_log_warning(logger, "Failed to read the ROCm SMI ECC error count")
548
+ return DeviceMemoryStatusEnum.HEALTHY
555
549
 
556
550
  return DeviceMemoryStatusEnum.HEALTHY
557
551
 
@@ -6,6 +6,7 @@ import os
6
6
  import re
7
7
  from functools import lru_cache
8
8
  from pathlib import Path
9
+ from typing import TYPE_CHECKING
9
10
 
10
11
  from .. import envs
11
12
  from ..logging import debug_log_exception, debug_log_warning
@@ -30,6 +31,9 @@ from .__utils__ import (
30
31
  map_numa_node_to_cpu_affinity,
31
32
  )
32
33
 
34
+ if TYPE_CHECKING:
35
+ from collections.abc import Callable
36
+
33
37
  logger = logging.getLogger(__name__)
34
38
  slogger = logger.getChild("internal")
35
39
 
@@ -788,33 +792,109 @@ def _get_device_memory_info_v2(dev_id) -> tuple[int, int]:
788
792
  return dev_hbm_info.memory_size, dev_hbm_info.memory_usage
789
793
 
790
794
 
795
+ def _unhealthy(unhealthy: bool) -> DeviceMemoryStatusEnum:
796
+ """
797
+ Map a verdict to the device's health status.
798
+
799
+ Args:
800
+ unhealthy:
801
+ Whether the device is unhealthy.
802
+
803
+ Returns:
804
+ DeviceMemoryStatusEnum.UNHEALTHY if the device is unhealthy,
805
+ HEALTHY otherwise.
806
+
807
+ """
808
+ if unhealthy:
809
+ return DeviceMemoryStatusEnum.UNHEALTHY
810
+ return DeviceMemoryStatusEnum.HEALTHY
811
+
812
+
813
+ def _evaluate_device_unhealthy(
814
+ health: int | None,
815
+ read_ecc: Callable[[], pydcmi.c_dcmi_ecc_info],
816
+ ) -> DeviceMemoryStatusEnum:
817
+ """
818
+ Decide whether a device is unhealthy, the way the operator does.
819
+
820
+ The DCMI health register is the primary signal: 0 is Normal, 1 is Minor,
821
+ and 2 or more is Major, Critical or Not Found. The ECC counters are a
822
+ fallback for a device whose health cannot be read, and only an enabled
823
+ double-bit error makes it unhealthy. A single-bit error is a corrected
824
+ error and the device still works, and an unreadable counter says nothing
825
+ about the device at all.
826
+
827
+ Args:
828
+ health:
829
+ The device's health status, or None if the query failed.
830
+ read_ecc:
831
+ Reads the ECC counters of the device's memory type. It is called
832
+ only when the health query failed.
833
+
834
+ Returns:
835
+ DeviceMemoryStatusEnum indicating the device's health.
836
+
837
+ """
838
+ if health is not None:
839
+ return _unhealthy(health >= 2)
840
+
841
+ try:
842
+ ecc_info = read_ecc()
843
+ except pydcmi.DCMIError:
844
+ debug_log_exception(logger, "Failed to read the ECC counters of the device")
845
+ return DeviceMemoryStatusEnum.HEALTHY
846
+
847
+ return _unhealthy(ecc_info.enable_flag > 0 and ecc_info.double_bit_error_cnt > 0)
848
+
849
+
850
+ def _read_device_health(read_health: Callable[[], int]) -> int | None:
851
+ """
852
+ Read the health register of a device.
853
+
854
+ Args:
855
+ read_health:
856
+ Reads the health status of one device.
857
+
858
+ Returns:
859
+ The device's health status, or None if the query failed.
860
+
861
+ """
862
+ try:
863
+ return read_health()
864
+ except pydcmi.DCMIError:
865
+ debug_log_exception(logger, "Failed to read the health of the device")
866
+ return None
867
+
868
+
791
869
  def _get_device_memory_status_v2(dev_id) -> DeviceMemoryStatusEnum:
792
870
  """
793
- Get device memory ECC status through the V2 API.
871
+ Get the device's health status through the V2 API.
872
+
873
+ The health register decides, as it does in the operator's
874
+ evaluateDeviceUnhealthy: the device is unhealthy if and only if the
875
+ status is 2 or more. The ECC counters are read only when the health
876
+ query fails. V2 declares the HBM call alone, so the counters of that
877
+ memory type are the only ones a device can have.
794
878
 
795
879
  Args:
796
880
  dev_id:
797
881
  The device ID of the device.
798
882
 
799
883
  Returns:
800
- DeviceMemoryStatusEnum indicating the ECC status.
884
+ DeviceMemoryStatusEnum indicating the device's health.
801
885
 
802
886
  """
803
- if not envs.GPUSTACK_RUNTIME_DETECT_NO_HEALTH_CHECK:
804
- for dev_mem_type in [pydcmi.DCMI_DEVICE_TYPE_HBM, pydcmi.DCMI_DEVICE_TYPE_DDR]:
805
- with contextlib.suppress(pydcmi.DCMIError):
806
- dev_ecc_info = pydcmi.dcmiv2_get_device_ecc_info(
807
- dev_id,
808
- dev_mem_type,
809
- )
810
- if dev_ecc_info.enable_flag and (
811
- dev_ecc_info.single_bit_error_cnt > 0
812
- or dev_ecc_info.double_bit_error_cnt > 0
813
- ):
814
- return DeviceMemoryStatusEnum.UNHEALTHY
815
- return DeviceMemoryStatusEnum.HEALTHY
816
-
817
- return DeviceMemoryStatusEnum.HEALTHY
887
+ if envs.GPUSTACK_RUNTIME_DETECT_NO_HEALTH_CHECK:
888
+ return DeviceMemoryStatusEnum.HEALTHY
889
+
890
+ health = _read_device_health(lambda: pydcmi.dcmiv2_get_device_health(dev_id))
891
+ return _evaluate_device_unhealthy(
892
+ health,
893
+ lambda: pydcmi.dcmiv2_get_device_ecc_info(
894
+ dev_id,
895
+ pydcmi.DCMI_DEVICE_TYPE_HBM,
896
+ ),
897
+ )
818
898
 
819
899
 
820
900
  def _get_device_die(dev_card_id, dev_device_id) -> str:
@@ -964,9 +1044,58 @@ def _get_device_memory_info_without_hbm(dev_card_id, dev_device_id) -> tuple[int
964
1044
  )
965
1045
 
966
1046
 
1047
+ def _read_ecc_info_v1(dev_card_id, dev_device_id) -> pydcmi.c_dcmi_ecc_info:
1048
+ """
1049
+ Read the ECC counters of the memory type the device uses.
1050
+
1051
+ The operator picks the memory type from the HBM query: a device that
1052
+ answers it has HBM, and one that does not has DDR. Only that one memory
1053
+ type is read, where the old code looped over both. A device whose HBM
1054
+ query answers with no memory has no HBM either, the same way
1055
+ _get_device_memory_info reads it.
1056
+
1057
+ Args:
1058
+ dev_card_id:
1059
+ The card ID of the device.
1060
+ dev_device_id:
1061
+ The device ID of the device.
1062
+
1063
+ Returns:
1064
+ The ECC counters of the device's memory type.
1065
+
1066
+ Raises:
1067
+ pydcmi.DCMIError: If the driver refuses the ECC query.
1068
+
1069
+ """
1070
+ dev_mem_type = pydcmi.DCMI_DEVICE_TYPE_DDR
1071
+ try:
1072
+ dev_hbm_info = pydcmi.dcmi_get_device_hbm_info(dev_card_id, dev_device_id)
1073
+ if dev_hbm_info.memory_size > 0:
1074
+ dev_mem_type = pydcmi.DCMI_DEVICE_TYPE_HBM
1075
+ except pydcmi.DCMIError:
1076
+ # No HBM means a DDR device, which is what the operator falls back to.
1077
+ debug_log_exception(
1078
+ logger,
1079
+ "Failed to read the HBM info of device %d/%d, reading its DDR counters",
1080
+ dev_card_id,
1081
+ dev_device_id,
1082
+ )
1083
+
1084
+ return pydcmi.dcmi_get_device_ecc_info(
1085
+ dev_card_id,
1086
+ dev_device_id,
1087
+ dev_mem_type,
1088
+ )
1089
+
1090
+
967
1091
  def _get_device_memory_status(dev_card_id, dev_device_id) -> DeviceMemoryStatusEnum:
968
1092
  """
969
- Get device memory ECC status.
1093
+ Get the device's health status through the V1 API.
1094
+
1095
+ The health register decides, as it does in the operator's
1096
+ evaluateDeviceUnhealthy: the device is unhealthy if and only if the
1097
+ status is 2 or more. The ECC counters are read only when the health
1098
+ query fails, and then only for the memory type the device uses.
970
1099
 
971
1100
  Args:
972
1101
  dev_card_id:
@@ -975,25 +1104,19 @@ def _get_device_memory_status(dev_card_id, dev_device_id) -> DeviceMemoryStatusE
975
1104
  The device ID of the device.
976
1105
 
977
1106
  Returns:
978
- DeviceMemoryStatusEnum indicating the ECC status.
1107
+ DeviceMemoryStatusEnum indicating the device's health.
979
1108
 
980
1109
  """
981
- if not envs.GPUSTACK_RUNTIME_DETECT_NO_HEALTH_CHECK:
982
- for dev_mem_type in [pydcmi.DCMI_DEVICE_TYPE_HBM, pydcmi.DCMI_DEVICE_TYPE_DDR]:
983
- with contextlib.suppress(pydcmi.DCMIError):
984
- dev_ecc_info = pydcmi.dcmi_get_device_ecc_info(
985
- dev_card_id,
986
- dev_device_id,
987
- dev_mem_type,
988
- )
989
- if dev_ecc_info.enable_flag and (
990
- dev_ecc_info.single_bit_error_cnt > 0
991
- or dev_ecc_info.double_bit_error_cnt > 0
992
- ):
993
- return DeviceMemoryStatusEnum.UNHEALTHY
994
- return DeviceMemoryStatusEnum.HEALTHY
1110
+ if envs.GPUSTACK_RUNTIME_DETECT_NO_HEALTH_CHECK:
1111
+ return DeviceMemoryStatusEnum.HEALTHY
995
1112
 
996
- return DeviceMemoryStatusEnum.HEALTHY
1113
+ health = _read_device_health(
1114
+ lambda: pydcmi.dcmi_get_device_health(dev_card_id, dev_device_id),
1115
+ )
1116
+ return _evaluate_device_unhealthy(
1117
+ health,
1118
+ lambda: _read_ecc_info_v1(dev_card_id, dev_device_id),
1119
+ )
997
1120
 
998
1121
 
999
1122
  def _get_device_roce_network_info(
@@ -313,6 +313,11 @@ def _get_memory_status(device: int) -> DeviceMemoryStatusEnum:
313
313
  (`memoryUnhealthy = healthInfo.Health == 0`): CNDev reports no per-memory
314
314
  health, and the ECC counters cndevGetECCInfo exposes take no part in it.
315
315
 
316
+ A failed V2 health read falls back to the V1 one, for any reason and not
317
+ only for a missing V2 symbol, as the operator's evaluateCardUnhealthy does:
318
+ a driver refusing the V2 API still yields a verdict instead of a blank one.
319
+ Two unreadable reads yield no verdict, which is healthy.
320
+
316
321
  Args:
317
322
  device:
318
323
  The device handle.
@@ -324,10 +329,21 @@ def _get_memory_status(device: int) -> DeviceMemoryStatusEnum:
324
329
  if envs.GPUSTACK_RUNTIME_DETECT_NO_HEALTH_CHECK:
325
330
  return DeviceMemoryStatusEnum.HEALTHY
326
331
 
327
- with contextlib.suppress(pycndev.CNDevError):
332
+ try:
328
333
  dev_health_state = pycndev.cndevGetCardHealthStateV2(device)
329
- if dev_health_state.health == 0:
330
- return DeviceMemoryStatusEnum.UNHEALTHY
334
+ except pycndev.CNDevError:
335
+ debug_log_warning(
336
+ logger,
337
+ "Failed to read the V2 card health state, falling back to V1",
338
+ )
339
+ try:
340
+ dev_health_state = pycndev.cndevGetCardHealthState(device)
341
+ except pycndev.CNDevError:
342
+ debug_log_exception(logger, "Failed to read the card health state")
343
+ return DeviceMemoryStatusEnum.HEALTHY
344
+
345
+ if dev_health_state.health == 0:
346
+ return DeviceMemoryStatusEnum.UNHEALTHY
331
347
 
332
348
  return DeviceMemoryStatusEnum.HEALTHY
333
349