gpustack-runtime 0.2.3__tar.gz → 0.2.3.post1__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 (150) hide show
  1. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/PKG-INFO +1 -1
  2. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/_version.py +2 -2
  3. gpustack_runtime-0.2.3.post1/gpustack_runtime/_version_appendix.py +1 -0
  4. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/__types__.py +30 -1
  5. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/docker.py +11 -5
  6. gpustack_runtime-0.2.3.post1/gpustack_runtime/deployer/k8s/devicemanager/__init__.py +149 -0
  7. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/kuberentes.py +95 -12
  8. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/podman.py +11 -5
  9. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_privileged.py +2 -6
  10. gpustack_runtime-0.2.3.post1/tests/gpustack_runtime/deployer/test_resource_injection_policy.py +208 -0
  11. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_visible_devices_ordering.py +32 -1
  12. gpustack_runtime-0.2.3/gpustack_runtime/_version_appendix.py +0 -1
  13. gpustack_runtime-0.2.3/gpustack_runtime/deployer/k8s/devicemanager/__init__.py +0 -78
  14. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.codespelldict +0 -0
  15. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.codespellrc +0 -0
  16. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.dockerignore +0 -0
  17. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.gitattributes +0 -0
  18. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.gitignore +0 -0
  19. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.pre-commit-config.yaml +0 -0
  20. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.python-version +0 -0
  21. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/LICENSE +0 -0
  22. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/Makefile +0 -0
  23. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/README.md +0 -0
  24. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/deploy/manifests/docker-compose.yaml +0 -0
  25. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/deploy/manifests/kubernetes.yaml +0 -0
  26. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/docs/index.md +0 -0
  27. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/docs/modules/gpustack_runtime.deployer.md +0 -0
  28. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/docs/modules/gpustack_runtime.detector.md +0 -0
  29. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/docs/modules/gpustack_runtime.md +0 -0
  30. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/__init__.py +0 -0
  31. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/__main__.py +0 -0
  32. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/_version.pyi +0 -0
  33. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/__init__.py +0 -0
  34. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/__types__.py +0 -0
  35. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/deployer.py +0 -0
  36. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/detector.py +0 -0
  37. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/images.py +0 -0
  38. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/__init__.py +0 -0
  39. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/__patches__.py +0 -0
  40. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/__utils__.py +0 -0
  41. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/__init__.py +0 -0
  42. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/__types__.py +0 -0
  43. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/__utils__.py +0 -0
  44. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/amd.py +0 -0
  45. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/ascend.py +0 -0
  46. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/hygon.py +0 -0
  47. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/iluvatar.py +0 -0
  48. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/metax.py +0 -0
  49. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/thead.py +0 -0
  50. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/__init__.py +0 -0
  51. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/__types__.py +0 -0
  52. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/__utils__.py +0 -0
  53. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/amd.py +0 -0
  54. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/ascend.py +0 -0
  55. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/cambricon.py +0 -0
  56. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/hygon.py +0 -0
  57. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/iluvatar.py +0 -0
  58. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/metax.py +0 -0
  59. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/mthreads.py +0 -0
  60. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/nvidia.py +0 -0
  61. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyamdgpu/__init__.py +0 -0
  62. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyamdsmi/__init__.py +0 -0
  63. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pycndev/__init__.py +0 -0
  64. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pydcmi/__init__.py +0 -0
  65. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyhgml/__init__.py +0 -0
  66. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyhgml/libhgml.so +0 -0
  67. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyhgml/libuki.so +0 -0
  68. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyhsa/__init__.py +0 -0
  69. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyixml/__init__.py +0 -0
  70. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pymtml/__init__.py +0 -0
  71. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pymxsml/__init__.py +0 -0
  72. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pynvml/__init__.py +0 -0
  73. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyrocmsmi/__init__.py +0 -0
  74. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/thead.py +0 -0
  75. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/envs.py +0 -0
  76. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/logging.py +0 -0
  77. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/hatch.toml +0 -0
  78. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/mkdocs.yml +0 -0
  79. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/pack/Dockerfile +0 -0
  80. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/pack/Dockerfile.dummy +0 -0
  81. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/pyproject.toml +0 -0
  82. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/pytest.ini +0 -0
  83. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/ruff.toml +0 -0
  84. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/specs/2026-08-14-detector-alignment-and-workload-exit-status.md +0 -0
  85. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/__init__.py +0 -0
  86. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_compare_versions.json +0 -0
  87. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_correct_runner_image.json +0 -0
  88. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json.json +0 -0
  89. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_multiple_jsons.json +0 -0
  90. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_multiple_yamls.yaml +0 -0
  91. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_single_json.json +0 -0
  92. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_single_yaml.yaml +0 -0
  93. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_nginx_entrypoint.sh +0 -0
  94. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_docker_status.py +0 -0
  95. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_kubernetes_status.py +0 -0
  96. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_podman_status.py +0 -0
  97. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_runtime_class.py +0 -0
  98. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_utils.py +0 -0
  99. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_workload_status.py +0 -0
  100. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/fixtures/__init__.py +0 -0
  101. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/README.md +0 -0
  102. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi300x.json +0 -0
  103. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi308x.json +0 -0
  104. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_amd_rx7800xt.json +0 -0
  105. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_ascend_310p3.json +0 -0
  106. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_ascend_910b2.json +0 -0
  107. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_hygon_k100ai.json +0 -0
  108. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_metax_c500.json +0 -0
  109. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_gb10.json +0 -0
  110. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100.json +0 -0
  111. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100_mig.json +0 -0
  112. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h200.json +0 -0
  113. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4080super.json +0 -0
  114. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090_48g.json +0 -0
  115. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090d.json +0 -0
  116. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx5090d.json +0 -0
  117. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_thead_ppu.json +0 -0
  118. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi300x.json +0 -0
  119. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi308x.json +0 -0
  120. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_amd_rx7800xt.json +0 -0
  121. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_ascend_310p3.json +0 -0
  122. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_ascend_910b2.json +0 -0
  123. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_hygon_k100ai.json +0 -0
  124. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_metax_c500.json +0 -0
  125. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_mthreads_s5000.json +0 -0
  126. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100.json +0 -0
  127. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100_mig.json +0 -0
  128. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h200.json +0 -0
  129. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4080super.json +0 -0
  130. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090_48g.json +0 -0
  131. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090d.json +0 -0
  132. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx5090d.json +0 -0
  133. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_thead_ppu.json +0 -0
  134. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_amd.py +0 -0
  135. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_ascend.py +0 -0
  136. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_cambricon.py +0 -0
  137. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_detector_cli.py +0 -0
  138. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_detector_types.py +0 -0
  139. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_detector_utils.py +0 -0
  140. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_hygon.py +0 -0
  141. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_iluvatar.py +0 -0
  142. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_metax.py +0 -0
  143. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_mig_devices.py +0 -0
  144. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_mthreads.py +0 -0
  145. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_nvidia.py +0 -0
  146. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_pycndev.py +0 -0
  147. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_samples.py +0 -0
  148. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_thead.py +0 -0
  149. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/uv.lock +0 -0
  150. {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/uv.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gpustack-runtime
3
- Version: 0.2.3
3
+ Version: 0.2.3.post1
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.3'
31
- __version_tuple__ = version_tuple = (0, 2, 3)
30
+ __version__ = version = '0.2.3.post1'
31
+ __version_tuple__ = version_tuple = (0, 2, 3, 'post1')
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 = "9364a69"
@@ -1787,6 +1787,35 @@ class Deployer(ABC):
1787
1787
  )
1788
1788
  return ret
1789
1789
 
1790
+ def count_requested_devices(
1791
+ self,
1792
+ runtime_envs: list[str],
1793
+ resource_values: list[str],
1794
+ ) -> int:
1795
+ """
1796
+ Count the devices a resource request resolves to.
1797
+
1798
+ "all" is a stand-in for every device the host has, so it is measured
1799
+ rather than counted as the single literal token it is written as.
1800
+
1801
+ Args:
1802
+ runtime_envs:
1803
+ The runtime visible devices environment variable names.
1804
+ resource_values:
1805
+ The resource values requested, as split from the resource
1806
+ value, e.g. ``["0", "1"]`` or ``["all"]``.
1807
+
1808
+ Returns:
1809
+ The number of devices the request resolves to.
1810
+
1811
+ """
1812
+ if resource_values == ["all"]:
1813
+ return sum(
1814
+ len(self.get_runtime_visible_devices(runtime_env, "plain"))
1815
+ for runtime_env in runtime_envs
1816
+ )
1817
+ return len(resource_values)
1818
+
1790
1819
  def map_visible_devices_ordering(
1791
1820
  self,
1792
1821
  runtime_envs: list[str],
@@ -1795,7 +1824,7 @@ class Deployer(ABC):
1795
1824
  Return the device ordering environment variables
1796
1825
  for the given runtime visible devices env names.
1797
1826
 
1798
- Only meaningful for a container seeing every device of the host:
1827
+ Only meaningful for a container seeing more than one device:
1799
1828
  it must number the devices as the detector, the driver and the vendor
1800
1829
  tooling do, otherwise an index computed from detection addresses
1801
1830
  another device inside the container.
@@ -1118,11 +1118,17 @@ class DockerDeployer(EndoscopicDeployer):
1118
1118
  )
1119
1119
  create_options["environment"].update(b_vs)
1120
1120
 
1121
- # If requesting all devices or privileged,
1122
- # the container sees every device of the host,
1123
- # so pin the device ordering to keep its numbering
1124
- # aligned with the detection.
1125
- if r_v == "all" or privileged:
1121
+ # Pin the device ordering whenever the container ends up
1122
+ # seeing more than one device, so its numbering stays
1123
+ # aligned with the detection. Requesting all devices is
1124
+ # measured rather than special-cased: a single-device host
1125
+ # has nothing to reorder. A privileged container sees every
1126
+ # device of the host whatever it requested.
1127
+ if (
1128
+ privileged
1129
+ or self.count_requested_devices(runtime_envs, resource_values)
1130
+ > 1
1131
+ ):
1126
1132
  o_vs = self.map_visible_devices_ordering(runtime_envs)
1127
1133
  # Take the ordering as default,
1128
1134
  # never overwrite the one declared by the container.
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations as __future_annotations__
2
+
3
+ import stat
4
+ from functools import lru_cache
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any, Literal
7
+
8
+ from gpustack_runtime import envs
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable, Mapping
12
+
13
+
14
+ def is_kubelet_socket_accessible(
15
+ kubelet_endpoint: Path | None = None,
16
+ ) -> bool:
17
+ """
18
+ Check if the kubelet socket is accessible.
19
+
20
+ Args:
21
+ kubelet_endpoint:
22
+ The path to the kubelet endpoint.
23
+
24
+ Returns:
25
+ True if the socket is accessible, False otherwise.
26
+
27
+ """
28
+ if not kubelet_endpoint:
29
+ kubelet_endpoint = Path("/var/lib/kubelet/device-plugins/kubelet.sock")
30
+
31
+ if kubelet_endpoint.exists():
32
+ path_stat = kubelet_endpoint.lstat()
33
+ if path_stat and stat.S_ISSOCK(path_stat.st_mode):
34
+ return True
35
+ return False
36
+
37
+
38
+ _DEVICE_PLUGIN_RESOURCE_FAMILIES = (
39
+ "shared",
40
+ "sliced",
41
+ "partitioned",
42
+ )
43
+ """
44
+ Resource-name families a device plugin advertises on top of a plain CDI kind,
45
+ mirroring the GPUStack Operator's own families
46
+ (`gpustack-operator pkg/nodefeature`): "nvidia.com/gpu.shared",
47
+ "nvidia.com/gpu.sliced.units", "nvidia.com/gpu.partitioned.mig-1g.20gb", ...
48
+ A stock vendor plugin advertises only the bare kind ("nvidia.com/gpu"), so the
49
+ family segment is what tells the two apart.
50
+ """
51
+
52
+
53
+ def node_has_device_plugin_resources(
54
+ node_allocatable: Mapping[str, Any],
55
+ ) -> bool:
56
+ """
57
+ Report whether a node advertises accelerators through a device plugin that
58
+ allocates them the way the GPUStack Operator does.
59
+
60
+ Only the suffixed families count (see
61
+ :data:`_DEVICE_PLUGIN_RESOURCE_FAMILIES`): a bare CDI kind on its own is
62
+ what a stock vendor plugin advertises, and requesting a device from it
63
+ yields none of the operator's accounting.
64
+
65
+ Args:
66
+ node_allocatable:
67
+ The allocatable resources of a node, keyed by resource name.
68
+
69
+ Returns:
70
+ True if any allocatable resource name carries a family segment.
71
+
72
+ """
73
+ return any(
74
+ family in name.split(".")
75
+ for name in node_allocatable
76
+ for family in _DEVICE_PLUGIN_RESOURCE_FAMILIES
77
+ )
78
+
79
+
80
+ def get_resource_injection_policy(
81
+ probe_node_allocatable: Callable[[], Mapping[str, Any] | None] | None = None,
82
+ ) -> Literal["env", "kdp"]:
83
+ """
84
+ Get the resource injection policy (in lowercase) for the deployer.
85
+
86
+ An explicit policy always wins. Under "auto" the decision belongs to the
87
+ cluster, not to the process doing the deploying: the Kubernetes deployer
88
+ orchestrates remotely, so whether *it* can reach a kubelet socket says
89
+ nothing about whether the *target* node runs a device plugin. So the probe
90
+ reads that node's allocatable resources and looks for a device-plugin
91
+ resource family there.
92
+
93
+ A probe that cannot answer -- absent, failing, or unauthorized -- falls
94
+ back to KDP rather than to env: env injection hands the container every
95
+ device of the host and leaves the allocation off the plugin's ledger, so
96
+ guessing it wrong is the more damaging of the two.
97
+
98
+ Args:
99
+ probe_node_allocatable:
100
+ Called only under the "auto" policy, to read the target node's
101
+ allocatable resources. Returns None when the node cannot be read.
102
+
103
+ Returns:
104
+ The resource injection policy.
105
+
106
+ """
107
+ policy = envs.GPUSTACK_RUNTIME_KUBERNETES_RESOURCE_INJECTION_POLICY.lower()
108
+ if policy != "auto":
109
+ return policy
110
+
111
+ if probe_node_allocatable is None:
112
+ return "kdp"
113
+
114
+ node_allocatable = probe_node_allocatable()
115
+ if node_allocatable is None:
116
+ return "kdp"
117
+
118
+ return "kdp" if node_has_device_plugin_resources(node_allocatable) else "env"
119
+
120
+
121
+ @lru_cache
122
+ def cdi_kind_to_kdp_resource(
123
+ cdi_kind: str,
124
+ mode: Literal["shared", "sliced"] = "shared",
125
+ ):
126
+ """
127
+ Convert a CDI kind to a KDP resource name.
128
+
129
+ Args:
130
+ cdi_kind:
131
+ The CDI kind to convert.
132
+ mode:
133
+ The mode of the resource, either "shared" or "sliced".
134
+
135
+ Returns:
136
+ The corresponding KDP resource name.
137
+
138
+ """
139
+ if mode == "shared":
140
+ return f"{cdi_kind}.shared"
141
+ return f"{cdi_kind}.sliced.units"
142
+
143
+
144
+ __all__ = [
145
+ "cdi_kind_to_kdp_resource",
146
+ "get_resource_injection_policy",
147
+ "is_kubelet_socket_accessible",
148
+ "node_has_device_plugin_resources",
149
+ ]
@@ -656,7 +656,7 @@ def _resolve_runtime_class_name(
656
656
  pod.spec.runtime_class_name = runtime_class_name
657
657
 
658
658
 
659
- def _resolve_privileged(container: Container) -> bool:
659
+ def _resolve_privileged(container: Container, kdp: bool) -> bool:
660
660
  """
661
661
  Resolve whether a container runs privileged.
662
662
 
@@ -670,13 +670,20 @@ def _resolve_privileged(container: Container) -> bool:
670
670
  slicing: a workload holding a single MIG device or a single memory slice
671
671
  still sees the untouched cards next to it, and a soft-slicing limit
672
672
  lands on whichever device comes first instead of the allocated one.
673
+
674
+ Args:
675
+ container:
676
+ The container to resolve.
677
+ kdp:
678
+ Whether the KDP injection policy is in effect, resolved once per
679
+ Pod so a workload's containers cannot disagree on it.
680
+
673
681
  """
674
682
  if not container.execution or not container.execution.privileged:
675
683
  return False
676
684
  if not container.resources:
677
685
  return True
678
686
 
679
- kdp = get_resource_injection_policy() == "kdp"
680
687
  for r_k in container.resources:
681
688
  if r_k in ("cpu", "memory"):
682
689
  continue
@@ -1074,6 +1081,58 @@ class KubernetesDeployer(EndoscopicDeployer):
1074
1081
 
1075
1082
  return probe
1076
1083
 
1084
+ def _probe_node_allocatable(self) -> dict[str, str] | None:
1085
+ """
1086
+ Read the allocatable resources of the node this deployer targets,
1087
+ so the injection policy can tell whether a device plugin runs there.
1088
+
1089
+ Reads through ``list_node`` rather than ``read_node`` so it needs no
1090
+ permission beyond the list the deployer already requires to resolve a
1091
+ default node name. With no node configured it reads the same first node
1092
+ ``_get_default_node_name`` would, and remembers it, so resolving the
1093
+ default costs one call rather than two.
1094
+
1095
+ Returns:
1096
+ The node's allocatable resources, or None when the node cannot be
1097
+ read -- no permission, API error, or no node at all -- so the
1098
+ caller can tell "advertises nothing" from "could not look".
1099
+
1100
+ """
1101
+ core_api = kubernetes.client.CoreV1Api(self._client)
1102
+ try:
1103
+ nodes = core_api.list_node(
1104
+ field_selector=(
1105
+ f"metadata.name={self._node_name}" if self._node_name else None
1106
+ ),
1107
+ limit=1,
1108
+ )
1109
+ except kubernetes.client.exceptions.ApiException as e:
1110
+ clogger.warning(
1111
+ "Failed to read node allocatable resources"
1112
+ "%s, assuming a device plugin is present",
1113
+ _detail_api_call_error(e),
1114
+ )
1115
+ return None
1116
+
1117
+ if not nodes.items:
1118
+ return None
1119
+
1120
+ node = nodes.items[0]
1121
+ if not self._node_name:
1122
+ self._node_name = node.metadata.name
1123
+ return node.status.allocatable or {}
1124
+
1125
+ def _resolve_resource_injection_policy(self) -> str:
1126
+ """
1127
+ Resolve the resource injection policy for this deployer, probing the
1128
+ target node when the configured policy is "auto".
1129
+
1130
+ Returns:
1131
+ The resource injection policy.
1132
+
1133
+ """
1134
+ return get_resource_injection_policy(self._probe_node_allocatable)
1135
+
1077
1136
  def _get_default_node_name(self) -> str:
1078
1137
  """
1079
1138
  Get the default node name of the cluster.
@@ -1386,6 +1445,10 @@ class KubernetesDeployer(EndoscopicDeployer):
1386
1445
  ephemeral_filename_mapping,
1387
1446
  )
1388
1447
 
1448
+ # Resolve the injection policy once per Pod: under "auto" it probes the
1449
+ # target node, and every container of the Pod lands on that same node.
1450
+ kdp = self._resolve_resource_injection_policy() == "kdp"
1451
+
1389
1452
  cnt_init, cnt_run = -1, -1
1390
1453
  for ci, c in enumerate(workload.containers):
1391
1454
  # Annotate container info.
@@ -1421,7 +1484,7 @@ class KubernetesDeployer(EndoscopicDeployer):
1421
1484
  run_as_user=c.execution.run_as_user,
1422
1485
  run_as_group=c.execution.run_as_group,
1423
1486
  read_only_root_filesystem=c.execution.readonly_rootfs,
1424
- privileged=_resolve_privileged(c),
1487
+ privileged=_resolve_privileged(c, kdp),
1425
1488
  capabilities=(
1426
1489
  kubernetes.client.V1Capabilities(
1427
1490
  add=c.execution.capabilities.add,
@@ -1441,7 +1504,6 @@ class KubernetesDeployer(EndoscopicDeployer):
1441
1504
 
1442
1505
  # Parameterize resources
1443
1506
  if c.resources:
1444
- kdp = get_resource_injection_policy() == "kdp"
1445
1507
  fmt = "kdp" if kdp else "plain"
1446
1508
 
1447
1509
  resources: dict[str, str] = {}
@@ -1546,15 +1608,26 @@ class KubernetesDeployer(EndoscopicDeployer):
1546
1608
  ],
1547
1609
  )
1548
1610
 
1549
- # If requesting all devices or privileged,
1550
- # the container sees every device of the host,
1551
- # so pin the device ordering to keep its numbering
1611
+ # Pin the device ordering whenever the container ends up
1612
+ # seeing more than one device, so its numbering stays
1552
1613
  # aligned with the detection.
1553
- # This includes requesting all devices under KDP:
1554
- # the device plugin allocates every device of the node,
1555
- # hence the container still enumerates all of them.
1614
+ # That covers every multi-device request, not only "all":
1615
+ # any container holding several devices numbers them
1616
+ # itself, and a performance-sorted default reshuffles those
1617
+ # ordinals on a heterogeneous host.
1618
+ # Requesting all devices is measured rather than
1619
+ # special-cased -- including under KDP, where the device
1620
+ # plugin allocates every device of the node and the
1621
+ # container still enumerates all of them -- because a
1622
+ # single-device host has nothing to reorder.
1623
+ # A privileged container sees every device of the host
1624
+ # whatever it requested, so it pins regardless.
1556
1625
  # Never overwrite the ordering declared by the container.
1557
- if r_v == "all" or privileged:
1626
+ if (
1627
+ privileged
1628
+ or self.count_requested_devices(runtime_envs, resource_values)
1629
+ > 1
1630
+ ):
1558
1631
  declared_envs = {e.name for e in container.env}
1559
1632
  container.env.extend(
1560
1633
  [
@@ -1700,10 +1773,20 @@ class KubernetesDeployer(EndoscopicDeployer):
1700
1773
  super().__init__(_NAME)
1701
1774
  self._client = self._get_client()
1702
1775
  self._node_name = envs.GPUSTACK_RUNTIME_KUBERNETES_NODE_NAME
1776
+ self._runtime_uuid_values_allowed: bool | None = None
1703
1777
 
1704
1778
  @property
1705
1779
  def allowed_runtime_uuid_values(self) -> bool:
1706
- return get_resource_injection_policy() != "kdp"
1780
+ # Resolved once per deployer, unlike the per-Pod resolution the
1781
+ # creation path wants: this gates how `_prepare` builds the device
1782
+ # materials, which are themselves built once, and it is read there once
1783
+ # per manufacturer -- so probing on every read would spend one API call
1784
+ # per manufacturer to answer a question already settled.
1785
+ if self._runtime_uuid_values_allowed is None:
1786
+ self._runtime_uuid_values_allowed = (
1787
+ self._resolve_resource_injection_policy() != "kdp"
1788
+ )
1789
+ return self._runtime_uuid_values_allowed
1707
1790
 
1708
1791
  @property
1709
1792
  def allowed_mig_devices(self) -> bool:
@@ -1097,11 +1097,17 @@ class PodmanDeployer(EndoscopicDeployer):
1097
1097
  )
1098
1098
  create_options["environment"].update(b_vs)
1099
1099
 
1100
- # If requesting all devices or privileged,
1101
- # the container sees every device of the host,
1102
- # so pin the device ordering to keep its numbering
1103
- # aligned with the detection.
1104
- if r_v == "all" or privileged:
1100
+ # Pin the device ordering whenever the container ends up
1101
+ # seeing more than one device, so its numbering stays
1102
+ # aligned with the detection. Requesting all devices is
1103
+ # measured rather than special-cased: a single-device host
1104
+ # has nothing to reorder. A privileged container sees every
1105
+ # device of the host whatever it requested.
1106
+ if (
1107
+ privileged
1108
+ or self.count_requested_devices(runtime_envs, resource_values)
1109
+ > 1
1110
+ ):
1105
1111
  o_vs = self.map_visible_devices_ordering(runtime_envs)
1106
1112
  # Take the ordering as default,
1107
1113
  # never overwrite the one declared by the container.
@@ -128,10 +128,6 @@ def _container(privileged: bool | None, resources: dict | None = None) -> Contai
128
128
  ),
129
129
  ],
130
130
  )
131
- def test_resolve_privileged(name, privileged, resources, policy, expected, monkeypatch):
132
- monkeypatch.setattr(
133
- "gpustack_runtime.deployer.kuberentes.get_resource_injection_policy",
134
- lambda: policy,
135
- )
136
- actual = _resolve_privileged(_container(privileged, resources))
131
+ def test_resolve_privileged(name, privileged, resources, policy, expected):
132
+ actual = _resolve_privileged(_container(privileged, resources), policy == "kdp")
137
133
  assert actual == expected, f"case {name} expected {expected}, but got {actual}"
@@ -0,0 +1,208 @@
1
+ # The deployer cases below drive its own probe and policy resolution, which are
2
+ # internal by design -- the API traffic they guard is not observable from the
3
+ # public surface.
4
+ # ruff: noqa: SLF001
5
+
6
+ from types import SimpleNamespace
7
+
8
+ import kubernetes.client
9
+ import pytest
10
+
11
+ from gpustack_runtime.deployer.k8s.devicemanager import (
12
+ get_resource_injection_policy,
13
+ node_has_device_plugin_resources,
14
+ )
15
+ from gpustack_runtime.deployer.kuberentes import KubernetesDeployer
16
+
17
+ # Allocatable of a node whose accelerators are advertised by a device plugin
18
+ # allocating them the way the GPUStack Operator does.
19
+ _OPERATOR_ALLOCATABLE = {
20
+ "cpu": "32",
21
+ "memory": "128Gi",
22
+ "nvidia.com/gpu": "1",
23
+ "nvidia.com/gpu.shared": "10",
24
+ "nvidia.com/gpu.sliced": "256",
25
+ "nvidia.com/gpu.sliced.units": "3200k",
26
+ "device.gpustack.ai/nvidia.visibility": "1024",
27
+ }
28
+
29
+ # Allocatable of a node running a stock vendor device plugin: the bare CDI kind
30
+ # and nothing else.
31
+ _STOCK_ALLOCATABLE = {
32
+ "cpu": "32",
33
+ "memory": "128Gi",
34
+ "nvidia.com/gpu": "2",
35
+ }
36
+
37
+
38
+ @pytest.mark.parametrize(
39
+ "name, allocatable, expected",
40
+ [
41
+ ("operator families", _OPERATOR_ALLOCATABLE, True),
42
+ ("bare CDI kind only", _STOCK_ALLOCATABLE, False),
43
+ ("no accelerator at all", {"cpu": "32", "memory": "128Gi"}, False),
44
+ ("nothing allocatable", {}, False),
45
+ # The visibility resource is deliberately outside the families, so on
46
+ # its own it must not pass for one.
47
+ (
48
+ "visibility resource only",
49
+ {"device.gpustack.ai/nvidia.visibility": "1024"},
50
+ False,
51
+ ),
52
+ # A partitioned family carries the profile after the family segment.
53
+ (
54
+ "partitioned profile key",
55
+ {"nvidia.com/gpu.partitioned.mig-1g.20gb": "7"},
56
+ True,
57
+ ),
58
+ ],
59
+ )
60
+ def test_node_has_device_plugin_resources(name, allocatable, expected):
61
+ actual = node_has_device_plugin_resources(allocatable)
62
+ assert actual == expected, f"case {name} expected {expected}, but got {actual}"
63
+
64
+
65
+ @pytest.mark.parametrize(
66
+ "name, configured, probe, expected",
67
+ [
68
+ # An explicit policy is never second-guessed, and never probes.
69
+ ("explicit env", "Env", None, "env"),
70
+ ("explicit kdp", "KDP", None, "kdp"),
71
+ (
72
+ "explicit env wins over an operator node",
73
+ "Env",
74
+ lambda: _OPERATOR_ALLOCATABLE,
75
+ "env",
76
+ ),
77
+ # Auto decides from the target node.
78
+ ("auto on an operator node", "Auto", lambda: _OPERATOR_ALLOCATABLE, "kdp"),
79
+ ("auto on a stock node", "Auto", lambda: _STOCK_ALLOCATABLE, "env"),
80
+ # A probe that cannot answer falls back to KDP: env injection hands the
81
+ # container every device of the host and leaves the allocation off the
82
+ # plugin's ledger, so guessing it wrong that way is the costlier miss.
83
+ ("auto with a failing probe", "Auto", lambda: None, "kdp"),
84
+ ("auto with no probe", "Auto", None, "kdp"),
85
+ ],
86
+ )
87
+ def test_get_resource_injection_policy(name, configured, probe, expected, monkeypatch):
88
+ monkeypatch.setattr(
89
+ "gpustack_runtime.envs.GPUSTACK_RUNTIME_KUBERNETES_RESOURCE_INJECTION_POLICY",
90
+ configured,
91
+ )
92
+ actual = get_resource_injection_policy(probe)
93
+ assert actual == expected, f"case {name} expected {expected}, but got {actual}"
94
+
95
+
96
+ def test_explicit_policy_does_not_probe(monkeypatch):
97
+ """An explicit policy must not spend an API call on the node."""
98
+ monkeypatch.setattr(
99
+ "gpustack_runtime.envs.GPUSTACK_RUNTIME_KUBERNETES_RESOURCE_INJECTION_POLICY",
100
+ "KDP",
101
+ )
102
+
103
+ calls = []
104
+
105
+ def _probe():
106
+ calls.append(1)
107
+ return _STOCK_ALLOCATABLE
108
+
109
+ assert get_resource_injection_policy(_probe) == "kdp"
110
+ assert calls == [], "the probe must not run under an explicit policy"
111
+
112
+
113
+ class _FakeNode:
114
+ def __init__(self, name, allocatable):
115
+ self.metadata = SimpleNamespace(name=name)
116
+ self.status = SimpleNamespace(allocatable=allocatable)
117
+
118
+
119
+ def _kubernetes_deployer(monkeypatch, nodes, node_name=None):
120
+ """A KubernetesDeployer whose only live dependency is list_node, counted."""
121
+ calls = []
122
+
123
+ class _FakeCoreV1Api:
124
+ def __init__(self, client=None):
125
+ pass
126
+
127
+ def list_node(self, field_selector=None, limit=None):
128
+ calls.append(field_selector)
129
+ items = nodes
130
+ if field_selector:
131
+ wanted = field_selector.split("=", 1)[1]
132
+ items = [n for n in nodes if n.metadata.name == wanted]
133
+ return SimpleNamespace(items=items[:limit] if limit else items)
134
+
135
+ monkeypatch.setattr(kubernetes.client, "CoreV1Api", _FakeCoreV1Api)
136
+
137
+ deployer = object.__new__(KubernetesDeployer)
138
+ deployer._client = None
139
+ deployer._node_name = node_name
140
+ deployer._runtime_uuid_values_allowed = None
141
+ return deployer, calls
142
+
143
+
144
+ def test_probe_remembers_the_node_it_resolved(monkeypatch):
145
+ """
146
+ With no node configured the probe reads the same first node
147
+ `_get_default_node_name` would, so it remembers it rather than leaving the
148
+ next caller to pay for the lookup again.
149
+ """
150
+ monkeypatch.setattr(
151
+ "gpustack_runtime.envs.GPUSTACK_RUNTIME_KUBERNETES_RESOURCE_INJECTION_POLICY",
152
+ "Auto",
153
+ )
154
+ deployer, calls = _kubernetes_deployer(
155
+ monkeypatch,
156
+ [_FakeNode("node-a", _OPERATOR_ALLOCATABLE)],
157
+ )
158
+
159
+ assert deployer._probe_node_allocatable() == _OPERATOR_ALLOCATABLE
160
+ assert deployer._node_name == "node-a"
161
+ # The next probe addresses that node by name instead of scanning again.
162
+ deployer._probe_node_allocatable()
163
+ assert calls == [None, "metadata.name=node-a"]
164
+
165
+
166
+ def test_probe_reports_none_when_the_node_cannot_be_read(monkeypatch):
167
+ deployer, _ = _kubernetes_deployer(monkeypatch, [])
168
+ assert deployer._probe_node_allocatable() is None
169
+ assert deployer._node_name is None
170
+
171
+
172
+ def test_allowed_runtime_uuid_values_probes_once(monkeypatch):
173
+ """
174
+ `_prepare` reads this once per manufacturer while building the device
175
+ materials, so it must not spend an API call per read.
176
+ """
177
+ monkeypatch.setattr(
178
+ "gpustack_runtime.envs.GPUSTACK_RUNTIME_KUBERNETES_RESOURCE_INJECTION_POLICY",
179
+ "Auto",
180
+ )
181
+ deployer, calls = _kubernetes_deployer(
182
+ monkeypatch,
183
+ [_FakeNode("node-a", _OPERATOR_ALLOCATABLE)],
184
+ node_name="node-a",
185
+ )
186
+
187
+ assert deployer.allowed_runtime_uuid_values is False # operator node -> kdp
188
+ assert deployer.allowed_runtime_uuid_values is False
189
+ assert deployer.allowed_runtime_uuid_values is False
190
+ assert len(calls) == 1, f"probed {len(calls)} times, expected 1"
191
+
192
+
193
+ def test_creation_path_still_probes_per_pod(monkeypatch):
194
+ """
195
+ The per-Pod resolution stays live: a node that gains (or loses) a device
196
+ plugin must be seen by the next deployment, not only by the next process.
197
+ """
198
+ monkeypatch.setattr(
199
+ "gpustack_runtime.envs.GPUSTACK_RUNTIME_KUBERNETES_RESOURCE_INJECTION_POLICY",
200
+ "Auto",
201
+ )
202
+ nodes = [_FakeNode("node-a", dict(_STOCK_ALLOCATABLE))]
203
+ deployer, calls = _kubernetes_deployer(monkeypatch, nodes, node_name="node-a")
204
+
205
+ assert deployer._resolve_resource_injection_policy() == "env"
206
+ nodes[0].status.allocatable = _OPERATOR_ALLOCATABLE
207
+ assert deployer._resolve_resource_injection_policy() == "kdp"
208
+ assert len(calls) == 2