gpustack-runtime 0.2.0.post5__tar.gz → 0.2.2__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 (132) hide show
  1. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/.gitignore +1 -0
  2. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/PKG-INFO +1 -1
  3. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/_version.py +2 -2
  4. gpustack_runtime-0.2.2/gpustack_runtime/_version_appendix.py +1 -0
  5. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/__types__.py +13 -0
  6. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/kuberentes.py +36 -0
  7. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/__utils__.py +32 -0
  8. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/ascend.py +1 -1
  9. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pyamdsmi/__init__.py +6 -1
  10. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pydcmi/__init__.py +6 -1
  11. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pyhgml/__init__.py +6 -1
  12. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pyixml/__init__.py +6 -1
  13. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pymtml/__init__.py +6 -1
  14. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pymxsml/__init__.py +12 -2
  15. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pynvml/__init__.py +6 -1
  16. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pyrocmsmi/__init__.py +6 -1
  17. gpustack_runtime-0.2.2/tests/gpustack_runtime/deployer/test_workload_status.py +117 -0
  18. gpustack_runtime-0.2.2/tests/gpustack_runtime/detector/test_detector_utils.py +170 -0
  19. gpustack_runtime-0.2.0.post5/gpustack_runtime/_version_appendix.py +0 -1
  20. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/.codespelldict +0 -0
  21. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/.codespellrc +0 -0
  22. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/.dockerignore +0 -0
  23. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/.gitattributes +0 -0
  24. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/.pre-commit-config.yaml +0 -0
  25. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/.python-version +0 -0
  26. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/LICENSE +0 -0
  27. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/Makefile +0 -0
  28. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/README.md +0 -0
  29. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/deploy/manifests/docker-compose.yaml +0 -0
  30. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/deploy/manifests/kubernetes.yaml +0 -0
  31. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/docs/index.md +0 -0
  32. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/docs/modules/gpustack_runtime.deployer.md +0 -0
  33. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/docs/modules/gpustack_runtime.detector.md +0 -0
  34. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/docs/modules/gpustack_runtime.md +0 -0
  35. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/__init__.py +0 -0
  36. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/__main__.py +0 -0
  37. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/_version.pyi +0 -0
  38. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/cmds/__init__.py +0 -0
  39. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/cmds/__types__.py +0 -0
  40. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/cmds/deployer.py +0 -0
  41. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/cmds/detector.py +0 -0
  42. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/cmds/images.py +0 -0
  43. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/__init__.py +0 -0
  44. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/__patches__.py +0 -0
  45. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/__utils__.py +0 -0
  46. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/cdi/__init__.py +0 -0
  47. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/cdi/__types__.py +0 -0
  48. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/cdi/__utils__.py +0 -0
  49. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/cdi/amd.py +0 -0
  50. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/cdi/ascend.py +0 -0
  51. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/cdi/hygon.py +0 -0
  52. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/cdi/iluvatar.py +0 -0
  53. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/cdi/metax.py +0 -0
  54. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/cdi/thead.py +0 -0
  55. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/docker.py +0 -0
  56. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/k8s/devicemanager/__init__.py +0 -0
  57. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/deployer/podman.py +0 -0
  58. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/__init__.py +0 -0
  59. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/__types__.py +0 -0
  60. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/amd.py +0 -0
  61. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/cambricon.py +0 -0
  62. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/hygon.py +0 -0
  63. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/iluvatar.py +0 -0
  64. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/metax.py +0 -0
  65. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/mthreads.py +0 -0
  66. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/nvidia.py +0 -0
  67. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pyamdgpu/__init__.py +0 -0
  68. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pyhgml/libhgml.so +0 -0
  69. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pyhgml/libuki.so +0 -0
  70. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/pyhsa/__init__.py +0 -0
  71. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/detector/thead.py +0 -0
  72. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/envs.py +0 -0
  73. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/gpustack_runtime/logging.py +0 -0
  74. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/hatch.toml +0 -0
  75. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/mkdocs.yml +0 -0
  76. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/pack/Dockerfile +0 -0
  77. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/pack/Dockerfile.dummy +0 -0
  78. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/pyproject.toml +0 -0
  79. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/pytest.ini +0 -0
  80. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/ruff.toml +0 -0
  81. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/fixtures/__init__.py +0 -0
  82. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/fixtures/test_compare_versions.json +0 -0
  83. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/fixtures/test_correct_runner_image.json +0 -0
  84. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json.json +0 -0
  85. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_multiple_jsons.json +0 -0
  86. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_multiple_yamls.yaml +0 -0
  87. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_single_json.json +0 -0
  88. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_single_yaml.yaml +0 -0
  89. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/fixtures/test_nginx_entrypoint.sh +0 -0
  90. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/deployer/test_utils.py +0 -0
  91. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/fixtures/__init__.py +0 -0
  92. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/README.md +0 -0
  93. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi300x.json +0 -0
  94. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi308x.json +0 -0
  95. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_amd_rx7800xt.json +0 -0
  96. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_ascend_310p3.json +0 -0
  97. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_ascend_910b2.json +0 -0
  98. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_hygon_k100ai.json +0 -0
  99. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_metax_c500.json +0 -0
  100. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_gb10.json +0 -0
  101. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100.json +0 -0
  102. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100_mig.json +0 -0
  103. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h200.json +0 -0
  104. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4080super.json +0 -0
  105. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090d.json +0 -0
  106. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx5090d.json +0 -0
  107. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/detect_output_thead_ppu.json +0 -0
  108. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi300x.json +0 -0
  109. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi308x.json +0 -0
  110. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_amd_rx7800xt.json +0 -0
  111. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_ascend_310p3.json +0 -0
  112. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_ascend_910b2.json +0 -0
  113. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_hygon_k100ai.json +0 -0
  114. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_metax_c500.json +0 -0
  115. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_mthreads_s5000.json +0 -0
  116. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100.json +0 -0
  117. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100_mig.json +0 -0
  118. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h200.json +0 -0
  119. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4080super.json +0 -0
  120. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090d.json +0 -0
  121. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx5090d.json +0 -0
  122. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/samples/topology_output_thead_ppu.json +0 -0
  123. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/test_amd.py +0 -0
  124. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/test_ascend.py +0 -0
  125. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/test_cambricon.py +0 -0
  126. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/test_hygon.py +0 -0
  127. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/test_iluvatar.py +0 -0
  128. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/test_metax.py +0 -0
  129. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/test_mthreads.py +0 -0
  130. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/tests/gpustack_runtime/detector/test_nvidia.py +0 -0
  131. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/uv.lock +0 -0
  132. {gpustack_runtime-0.2.0.post5 → gpustack_runtime-0.2.2}/uv.toml +0 -0
@@ -3,6 +3,7 @@
3
3
  .vscode/
4
4
  .DS_Store
5
5
  *.swp
6
+ .claude/
6
7
 
7
8
  # Python-generated files
8
9
  _version.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gpustack-runtime
3
- Version: 0.2.0.post5
3
+ Version: 0.2.2
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
@@ -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.0.post5'
31
- __version_tuple__ = version_tuple = (0, 2, 0, 'post5')
30
+ __version__ = version = '0.2.2'
31
+ __version_tuple__ = version_tuple = (0, 2, 2)
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 = "3ea35d7"
@@ -1135,6 +1135,10 @@ class WorkloadStatus:
1135
1135
  Namespace for the workload.
1136
1136
  labels (dict[str, str] | None):
1137
1137
  Labels for the workload.
1138
+ annotations (dict[str, str] | None):
1139
+ Annotations for the workload.
1140
+ state_message (str):
1141
+ Message accompanying the current state.
1138
1142
  executable (list[WorkloadStatusOperation]):
1139
1143
  The operation for the executable containers of the workload.
1140
1144
  loggable (list[WorkloadStatusOperation]):
@@ -1161,6 +1165,15 @@ class WorkloadStatus:
1161
1165
  """
1162
1166
  Labels for the workload.
1163
1167
  """
1168
+ annotations: dict[str, str] | None = field(default_factory=dict)
1169
+ """
1170
+ Annotations for the workload.
1171
+ """
1172
+ state_message: str = ""
1173
+ """
1174
+ Message accompanying the current state, e.g. the Kubernetes Pod's
1175
+ status message on failure (such as a device-plugin admission rejection).
1176
+ """
1164
1177
  executable: list[WorkloadStatusOperation] | None = field(default_factory=list)
1165
1178
  """
1166
1179
  The operation for the executable containers of the workload.
@@ -227,12 +227,18 @@ class KubernetesWorkloadStatus(WorkloadStatus):
227
227
  for k, v in (k_pod.metadata.labels or {}).items()
228
228
  if not k.startswith("runtime.gpustack.ai/")
229
229
  }
230
+ annotations = {
231
+ k: v
232
+ for k, v in (k_pod.metadata.annotations or {}).items()
233
+ if not k.startswith("runtime.gpustack.ai/")
234
+ }
230
235
 
231
236
  super().__init__(
232
237
  name=name,
233
238
  created_at=created_at,
234
239
  namespace=k_pod.metadata.namespace,
235
240
  labels=labels,
241
+ annotations=annotations,
236
242
  **kwargs,
237
243
  )
238
244
 
@@ -259,6 +265,11 @@ class KubernetesWorkloadStatus(WorkloadStatus):
259
265
  self.loggable.append(op)
260
266
 
261
267
  self.state = self.parse_state(k_pod)
268
+ if k_pod.status.message:
269
+ # Surface the Pod's status message, e.g. a device-plugin
270
+ # admission rejection ("UnexpectedAdmissionError: Allocate
271
+ # failed ...") on Failed Pods.
272
+ self.state_message = k_pod.status.message
262
273
 
263
274
 
264
275
  _NAME = "kubernetes"
@@ -267,6 +278,29 @@ Name of the Kubernetes deployer.
267
278
  """
268
279
 
269
280
 
281
+ def _pin_pod_for_kueue(
282
+ pod: kubernetes.client.V1Pod,
283
+ node_name: str | None,
284
+ ) -> None:
285
+ """
286
+ Adjust node pinning for Kueue-gated Pods in place.
287
+
288
+ Kueue-gated Pods (queue label present) cannot set spec.nodeName:
289
+ the API forbids nodeName until all schedulingGates have been
290
+ cleared. Pin via nodeSelector instead; kube-scheduler places
291
+ the Pod on the same node once Kueue admits it.
292
+ """
293
+ if not node_name:
294
+ return
295
+ if "kueue.x-k8s.io/queue-name" not in (pod.metadata.labels or {}):
296
+ return
297
+ pod.spec.node_name = None
298
+ pod.spec.node_selector = {
299
+ **(pod.spec.node_selector or {}),
300
+ "kubernetes.io/hostname": node_name,
301
+ }
302
+
303
+
270
304
  class KubernetesDeployer(EndoscopicDeployer):
271
305
  """
272
306
  Deployer implementation for Kubernetes.
@@ -1191,6 +1225,8 @@ class KubernetesDeployer(EndoscopicDeployer):
1191
1225
 
1192
1226
  core_api = kubernetes.client.CoreV1Api(self._client)
1193
1227
  try:
1228
+ _pin_pod_for_kueue(pod, self._node_name)
1229
+
1194
1230
  pod = self._mutate_create_pod(pod)
1195
1231
  if envs.GPUSTACK_RUNTIME_DEPLOY_PRINT_CONVERSION:
1196
1232
  clogger.info(
@@ -16,6 +16,38 @@ from typing import Any
16
16
  from gpustack_runtime import envs
17
17
 
18
18
 
19
+ def clone_exception(exc: BaseException) -> BaseException:
20
+ """
21
+ Build a fresh copy of an exception, without its traceback or chaining state.
22
+
23
+ The detector library wrappers cache the init failure and re-raise it on every
24
+ subsequent call to fail fast. Re-raising the *same* object is a memory leak:
25
+ CPython appends a new frame to its ``__traceback__`` on each raise, and those
26
+ frames retain the caller's locals indefinitely (gpustack/gpustack#5342).
27
+
28
+ Cloning via ``BaseException.__new__`` preserves the original type and
29
+ attributes regardless of the class's constructor signature -- some binding
30
+ error types take an error code and leave ``args`` empty (e.g. amdsmi's
31
+ ``AmdSmiLibraryException``), so ``type(exc)(*exc.args)`` cannot reconstruct them.
32
+
33
+ Args:
34
+ exc:
35
+ The exception to clone.
36
+
37
+ Returns:
38
+ A fresh exception of the same type, carrying the same args and attributes
39
+ but no traceback or ``__cause__``/``__context__`` chain.
40
+
41
+ """
42
+ clone = BaseException.__new__(type(exc))
43
+ clone.args = exc.args
44
+ # Copy instance state (custom attributes, and __notes__ if any live here too).
45
+ # Guard against exotic exception types that don't expose __dict__.
46
+ if hasattr(exc, "__dict__"):
47
+ clone.__dict__.update(exc.__dict__)
48
+ return clone
49
+
50
+
19
51
  @dataclass
20
52
  class PCIDevice:
21
53
  address: str
@@ -658,5 +658,5 @@ def get_ascend_cann_variant(name: str | None) -> str | None:
658
658
  if version < 260:
659
659
  return "a3" # 910c/a3
660
660
  if version < 270:
661
- return "a5" # 910d/a5
661
+ return "950" # 950/a5
662
662
  return None
@@ -32,7 +32,12 @@ try:
32
32
 
33
33
  if _libInitialized:
34
34
  if _libInitializedException is not None:
35
- raise _libInitializedException
35
+ # Re-raise a fresh copy: re-raising the same cached exception object
36
+ # appends a traceback frame on every call, and those frames retain the
37
+ # caller's locals, leaking memory over time. See gpustack/gpustack#5342.
38
+ from ..__utils__ import clone_exception
39
+
40
+ raise clone_exception(_libInitializedException) from None
36
41
  return
37
42
 
38
43
  try:
@@ -780,7 +780,12 @@ def dcmi_init():
780
780
 
781
781
  if _libInitialized:
782
782
  if _libInitializedException is not None:
783
- raise _libInitializedException
783
+ # Re-raise a fresh copy: re-raising the same cached exception object
784
+ # appends a traceback frame on every call, and those frames retain the
785
+ # caller's locals, leaking memory over time. See gpustack/gpustack#5342.
786
+ from ..__utils__ import clone_exception
787
+
788
+ raise clone_exception(_libInitializedException) from None
784
789
  return
785
790
 
786
791
  try:
@@ -2380,7 +2380,12 @@ def hgmlInitWithFlags(flags):
2380
2380
 
2381
2381
  if _libInitialized:
2382
2382
  if _libInitializedException is not None:
2383
- raise _libInitializedException
2383
+ # Re-raise a fresh copy: re-raising the same cached exception object
2384
+ # appends a traceback frame on every call, and those frames retain the
2385
+ # caller's locals, leaking memory over time. See gpustack/gpustack#5342.
2386
+ from ..__utils__ import clone_exception
2387
+
2388
+ raise clone_exception(_libInitializedException) from None
2384
2389
  return
2385
2390
 
2386
2391
  try:
@@ -2131,7 +2131,12 @@ def nvmlInitWithFlags(flags):
2131
2131
 
2132
2132
  if _libInitialized:
2133
2133
  if _libInitializedException is not None:
2134
- raise _libInitializedException
2134
+ # Re-raise a fresh copy: re-raising the same cached exception object
2135
+ # appends a traceback frame on every call, and those frames retain the
2136
+ # caller's locals, leaking memory over time. See gpustack/gpustack#5342.
2137
+ from ..__utils__ import clone_exception
2138
+
2139
+ raise clone_exception(_libInitializedException) from None
2135
2140
  return
2136
2141
 
2137
2142
  try:
@@ -15,7 +15,12 @@ def mtmlLibraryInit():
15
15
 
16
16
  if _libInitialized:
17
17
  if _libInitializedException is not None:
18
- raise _libInitializedException
18
+ # Re-raise a fresh copy: re-raising the same cached exception object
19
+ # appends a traceback frame on every call, and those frames retain the
20
+ # caller's locals, leaking memory over time. See gpustack/gpustack#5342.
21
+ from ..__utils__ import clone_exception
22
+
23
+ raise clone_exception(_libInitializedException) from None
19
24
  return
20
25
 
21
26
  try:
@@ -778,7 +778,12 @@ def mxSmlInit():
778
778
 
779
779
  if _libInitialized:
780
780
  if _libInitializedException is not None:
781
- raise _libInitializedException
781
+ # Re-raise a fresh copy: re-raising the same cached exception object
782
+ # appends a traceback frame on every call, and those frames retain the
783
+ # caller's locals, leaking memory over time. See gpustack/gpustack#5342.
784
+ from ..__utils__ import clone_exception
785
+
786
+ raise clone_exception(_libInitializedException) from None
782
787
  return
783
788
 
784
789
  try:
@@ -802,7 +807,12 @@ def mxSmlInitWithFlags(flags):
802
807
 
803
808
  if _libInitialized:
804
809
  if _libInitializedException is not None:
805
- raise _libInitializedException
810
+ # Re-raise a fresh copy: re-raising the same cached exception object
811
+ # appends a traceback frame on every call, and those frames retain the
812
+ # caller's locals, leaking memory over time. See gpustack/gpustack#5342.
813
+ from ..__utils__ import clone_exception
814
+
815
+ raise clone_exception(_libInitializedException) from None
806
816
  return
807
817
 
808
818
  try:
@@ -16,7 +16,12 @@ def nvmlInitWithFlags(flags):
16
16
 
17
17
  if _libInitialized:
18
18
  if _libInitializedException is not None:
19
- raise _libInitializedException
19
+ # Re-raise a fresh copy: re-raising the same cached exception object
20
+ # appends a traceback frame on every call, and those frames retain the
21
+ # caller's locals, leaking memory over time. See gpustack/gpustack#5342.
22
+ from ..__utils__ import clone_exception
23
+
24
+ raise clone_exception(_libInitializedException) from None
20
25
  return
21
26
 
22
27
  try:
@@ -180,7 +180,12 @@ def rsmi_init(flags=0):
180
180
 
181
181
  if _libInitialized:
182
182
  if _libInitializedException is not None:
183
- raise _libInitializedException
183
+ # Re-raise a fresh copy: re-raising the same cached exception object
184
+ # appends a traceback frame on every call, and those frames retain the
185
+ # caller's locals, leaking memory over time. See gpustack/gpustack#5342.
186
+ from ..__utils__ import clone_exception
187
+
188
+ raise clone_exception(_libInitializedException) from None
184
189
  return
185
190
 
186
191
  try:
@@ -0,0 +1,117 @@
1
+ from datetime import datetime, timezone
2
+
3
+ import kubernetes.client
4
+
5
+ from gpustack_runtime.deployer.__types__ import WorkloadStatus
6
+ from gpustack_runtime.deployer.kuberentes import (
7
+ KubernetesWorkloadStatus,
8
+ _pin_pod_for_kueue,
9
+ )
10
+
11
+
12
+ def _pod(annotations=None) -> kubernetes.client.V1Pod:
13
+ return kubernetes.client.V1Pod(
14
+ metadata=kubernetes.client.V1ObjectMeta(
15
+ name="gpustack-test",
16
+ namespace="default",
17
+ creation_timestamp=datetime(2026, 7, 29, 8, 0, 0, tzinfo=timezone.utc),
18
+ labels={"app": "test"},
19
+ annotations=annotations,
20
+ ),
21
+ spec=kubernetes.client.V1PodSpec(
22
+ containers=[kubernetes.client.V1Container(name="run-0")],
23
+ ),
24
+ status=kubernetes.client.V1PodStatus(phase="Pending"),
25
+ )
26
+
27
+
28
+ def test_kubernetes_workload_status_annotations():
29
+ status = KubernetesWorkloadStatus(
30
+ name="test",
31
+ k_pod=_pod(
32
+ annotations={
33
+ "device.gpustack.ai/accelerator.allocated": '{"run-0": {}}',
34
+ "runtime.gpustack.ai/component-run-0-name": "internal",
35
+ },
36
+ ),
37
+ )
38
+ # Third-party annotations (e.g. device-plugin allocation) are surfaced,
39
+ # runtime-internal annotations are filtered out.
40
+ assert status.annotations == {
41
+ "device.gpustack.ai/accelerator.allocated": '{"run-0": {}}',
42
+ }
43
+
44
+
45
+ def test_kubernetes_workload_status_annotations_absent():
46
+ status = KubernetesWorkloadStatus(name="test", k_pod=_pod(annotations=None))
47
+ assert status.annotations == {}
48
+
49
+
50
+ def test_workload_status_annotations_default_empty():
51
+ # Docker/Podman deployers don't populate annotations; the base default is empty.
52
+ status = WorkloadStatus(name="test", created_at="2026-07-29T08:00:00.000000Z")
53
+ assert status.annotations == {}
54
+
55
+
56
+ def test_workload_status_annotations_json_roundtrip():
57
+ status = KubernetesWorkloadStatus(
58
+ name="test",
59
+ k_pod=_pod(annotations={"device.gpustack.ai/accelerator.allocated": "{}"}),
60
+ )
61
+ restored = WorkloadStatus.from_json(status.to_json())
62
+ assert restored.annotations == status.annotations
63
+
64
+
65
+ def _pinning_pod(labels=None) -> kubernetes.client.V1Pod:
66
+ return kubernetes.client.V1Pod(
67
+ metadata=kubernetes.client.V1ObjectMeta(name="p", labels=labels or {}),
68
+ spec=kubernetes.client.V1PodSpec(
69
+ node_name="node-a",
70
+ containers=[kubernetes.client.V1Container(name="run-0")],
71
+ ),
72
+ )
73
+
74
+
75
+ def test_pin_pod_for_kueue_switches_to_node_selector():
76
+ pod = _pinning_pod(labels={"kueue.x-k8s.io/queue-name": "q"})
77
+ _pin_pod_for_kueue(pod, "node-a")
78
+ assert pod.spec.node_name is None
79
+ assert pod.spec.node_selector == {"kubernetes.io/hostname": "node-a"}
80
+
81
+
82
+ def test_pin_pod_for_kueue_merges_existing_node_selector():
83
+ pod = _pinning_pod(labels={"kueue.x-k8s.io/queue-name": "q"})
84
+ pod.spec.node_selector = {"disk": "ssd"}
85
+ _pin_pod_for_kueue(pod, "node-a")
86
+ assert pod.spec.node_selector == {
87
+ "disk": "ssd",
88
+ "kubernetes.io/hostname": "node-a",
89
+ }
90
+
91
+
92
+ def test_pin_pod_for_kueue_noop_without_queue_label():
93
+ pod = _pinning_pod()
94
+ _pin_pod_for_kueue(pod, "node-a")
95
+ assert pod.spec.node_name == "node-a"
96
+ assert pod.spec.node_selector is None
97
+
98
+
99
+ def test_pin_pod_for_kueue_noop_without_node_name():
100
+ pod = _pinning_pod(labels={"kueue.x-k8s.io/queue-name": "q"})
101
+ _pin_pod_for_kueue(pod, None)
102
+ assert pod.spec.node_name == "node-a"
103
+
104
+
105
+ def test_kubernetes_workload_status_state_message():
106
+ pod = _pod()
107
+ pod.status = kubernetes.client.V1PodStatus(
108
+ phase="Failed",
109
+ message="Allocate failed due to no enough GPU devices",
110
+ )
111
+ status = KubernetesWorkloadStatus(name="test", k_pod=pod)
112
+ assert status.state_message == "Allocate failed due to no enough GPU devices"
113
+
114
+
115
+ def test_kubernetes_workload_status_state_message_default():
116
+ status = KubernetesWorkloadStatus(name="test", k_pod=_pod())
117
+ assert status.state_message == ""
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import ClassVar
5
+
6
+ import pytest
7
+
8
+ from gpustack_runtime.detector.__utils__ import clone_exception
9
+
10
+
11
+ # --------------------------------------------------------------------------- #
12
+ # Exception classes that mimic the real binding error types, to exercise the #
13
+ # constructor shapes clone_exception has to survive. #
14
+ # --------------------------------------------------------------------------- #
15
+ class RocmLikeError(Exception):
16
+ """Custom __init__ that does NOT call super().__init__ (like ROCMSMIError)."""
17
+
18
+ def __init__(self, value):
19
+ self.value = value
20
+
21
+ def __str__(self):
22
+ return f"rocm {self.value}"
23
+
24
+
25
+ class DcmiLikeError(Exception):
26
+ """Custom __new__(cls, value) that requires a positional arg (like DCMIError)."""
27
+
28
+ _mapping: ClassVar[dict] = {}
29
+
30
+ def __new__(cls, value):
31
+ target = DcmiLikeError._mapping.get(value, cls)
32
+ obj = Exception.__new__(target)
33
+ obj.value = value
34
+ return obj
35
+
36
+ def __str__(self):
37
+ return f"dcmi {self.value}"
38
+
39
+
40
+ class AmdSmiLikeError(Exception):
41
+ """super().__init__() with no args -> args == (); state lives in __dict__.
42
+
43
+ This is the case that `type(exc)(*exc.args)` CANNOT reconstruct.
44
+ """
45
+
46
+ def __init__(self, err_code):
47
+ super().__init__()
48
+ self.err_code = err_code
49
+ self.err_info = f"info-{err_code}"
50
+
51
+ def __str__(self):
52
+ return f"Error code: {self.err_code} | {self.err_info}"
53
+
54
+
55
+ def _tb_len(exc: BaseException) -> int:
56
+ n = 0
57
+ tb = exc.__traceback__
58
+ while tb:
59
+ n += 1
60
+ tb = tb.tb_next
61
+ return n
62
+
63
+
64
+ ALL_CASES = [
65
+ RocmLikeError(-99997),
66
+ DcmiLikeError(-8007),
67
+ AmdSmiLikeError(43),
68
+ ValueError("boom"),
69
+ RuntimeError("multi", "arg", 3),
70
+ ]
71
+
72
+
73
+ # --------------------------------------------------------------------------- #
74
+ # Correctness: the clone must be usable in place of the original. #
75
+ # --------------------------------------------------------------------------- #
76
+ def test_clone_preserves_type():
77
+ for exc in ALL_CASES:
78
+ clone = clone_exception(exc)
79
+ assert type(clone) is type(exc)
80
+
81
+
82
+ def test_clone_is_a_distinct_object():
83
+ for exc in ALL_CASES:
84
+ clone = clone_exception(exc)
85
+ assert clone is not exc
86
+
87
+
88
+ def test_clone_preserves_args_and_attributes_and_str():
89
+ for exc in ALL_CASES:
90
+ clone = clone_exception(exc)
91
+ assert clone.args == exc.args
92
+ assert clone.__dict__ == exc.__dict__
93
+ # str() relies on the preserved state; it must render identically.
94
+ assert str(clone) == str(exc)
95
+
96
+
97
+ def test_clone_still_matches_except_clause():
98
+ # A caller doing `except RocmLikeError` must still catch the clone.
99
+ with pytest.raises(RocmLikeError) as info:
100
+ raise clone_exception(RocmLikeError(-1))
101
+ assert info.value.value == -1
102
+
103
+
104
+ @pytest.mark.skipif(
105
+ sys.version_info < (3, 11),
106
+ reason="Exception.add_note() was added in Python 3.11",
107
+ )
108
+ def test_clone_preserves_notes():
109
+ # __notes__ (add_note, 3.11+) lives in __dict__, so it is carried over by
110
+ # the __dict__ copy without any special handling.
111
+ exc = ValueError("boom")
112
+ exc.add_note("first note")
113
+ exc.add_note("second note")
114
+ clone = clone_exception(exc)
115
+ assert clone.__notes__ == ["first note", "second note"]
116
+
117
+
118
+ def test_clone_handles_amdsmi_style_empty_args():
119
+ # The whole reason we don't use `type(exc)(*exc.args)`: it blows up here.
120
+ exc = AmdSmiLikeError(7)
121
+ assert exc.args == ()
122
+ clone = clone_exception(exc)
123
+ assert clone.err_code == 7
124
+ assert clone.err_info == "info-7"
125
+
126
+
127
+ # --------------------------------------------------------------------------- #
128
+ # The point of the helper: cloning + re-raising must NOT grow the cached #
129
+ # exception's traceback (this is the gpustack/gpustack#5342 regression). #
130
+ # --------------------------------------------------------------------------- #
131
+ def test_reraising_clones_does_not_grow_cached_traceback():
132
+ # Seed a traceback on the "cached" exception, like a first failed init.
133
+ with pytest.raises(RocmLikeError) as info:
134
+ raise RocmLikeError(-99997)
135
+ cached = info.value
136
+
137
+ baseline = _tb_len(cached)
138
+
139
+ raised = []
140
+ for _ in range(500):
141
+ with pytest.raises(RocmLikeError) as info:
142
+ raise clone_exception(cached) from None
143
+ raised.append(info.value)
144
+
145
+ # The cached object's traceback is untouched by the re-raises.
146
+ assert _tb_len(cached) == baseline
147
+ # Every raise produced a fresh object, never the cached one.
148
+ assert all(r is not cached for r in raised)
149
+
150
+
151
+ def test_contrast_reraising_same_object_would_grow_traceback():
152
+ # Demonstrates the bug the helper fixes: re-raising the SAME object grows
153
+ # its traceback unbounded. (Sanity check that our test measures the right
154
+ # thing; not testing clone_exception here.)
155
+ with pytest.raises(RocmLikeError) as info:
156
+ raise RocmLikeError(-99997)
157
+ cached = info.value
158
+
159
+ before = _tb_len(cached)
160
+ for _ in range(50):
161
+ with pytest.raises(RocmLikeError):
162
+ raise cached
163
+ assert _tb_len(cached) > before
164
+
165
+
166
+ def test_clone_severs_cause_and_context_when_raised_from_none():
167
+ with pytest.raises(ValueError, match="x") as info:
168
+ raise clone_exception(ValueError("x")) from None
169
+ assert info.value.__cause__ is None
170
+ assert info.value.__suppress_context__ is True
@@ -1 +0,0 @@
1
- git_commit = "94ebfdd"