gpustack-runtime 0.2.3.post1__tar.gz → 0.2.4__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.post1 → gpustack_runtime-0.2.4}/PKG-INFO +1 -1
  2. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/_version.py +2 -2
  3. gpustack_runtime-0.2.4/gpustack_runtime/_version_appendix.py +1 -0
  4. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/cmds/deployer.py +21 -0
  5. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/__init__.py +11 -1
  6. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/__types__.py +40 -1
  7. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/cdi/__utils__.py +81 -0
  8. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/cdi/ascend.py +64 -7
  9. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/docker.py +114 -8
  10. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/kuberentes.py +143 -9
  11. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/podman.py +117 -8
  12. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/ascend.py +389 -3
  13. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pydcmi/__init__.py +180 -1
  14. gpustack_runtime-0.2.4/tests/gpustack_runtime/deployer/test_termination_grace_period.py +728 -0
  15. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_ascend.py +711 -0
  16. gpustack_runtime-0.2.3.post1/gpustack_runtime/_version_appendix.py +0 -1
  17. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/.codespelldict +0 -0
  18. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/.codespellrc +0 -0
  19. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/.dockerignore +0 -0
  20. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/.gitattributes +0 -0
  21. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/.gitignore +0 -0
  22. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/.pre-commit-config.yaml +0 -0
  23. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/.python-version +0 -0
  24. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/LICENSE +0 -0
  25. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/Makefile +0 -0
  26. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/README.md +0 -0
  27. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/deploy/manifests/docker-compose.yaml +0 -0
  28. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/deploy/manifests/kubernetes.yaml +0 -0
  29. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/docs/index.md +0 -0
  30. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/docs/modules/gpustack_runtime.deployer.md +0 -0
  31. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/docs/modules/gpustack_runtime.detector.md +0 -0
  32. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/docs/modules/gpustack_runtime.md +0 -0
  33. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/__init__.py +0 -0
  34. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/__main__.py +0 -0
  35. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/_version.pyi +0 -0
  36. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/cmds/__init__.py +0 -0
  37. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/cmds/__types__.py +0 -0
  38. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/cmds/detector.py +0 -0
  39. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/cmds/images.py +0 -0
  40. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/__patches__.py +0 -0
  41. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/__utils__.py +0 -0
  42. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/cdi/__init__.py +0 -0
  43. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/cdi/__types__.py +0 -0
  44. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/cdi/amd.py +0 -0
  45. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/cdi/hygon.py +0 -0
  46. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/cdi/iluvatar.py +0 -0
  47. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/cdi/metax.py +0 -0
  48. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/cdi/thead.py +0 -0
  49. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/deployer/k8s/devicemanager/__init__.py +0 -0
  50. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/__init__.py +0 -0
  51. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/__types__.py +0 -0
  52. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/__utils__.py +0 -0
  53. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/amd.py +0 -0
  54. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/cambricon.py +0 -0
  55. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/hygon.py +0 -0
  56. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/iluvatar.py +0 -0
  57. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/metax.py +0 -0
  58. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/mthreads.py +0 -0
  59. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/nvidia.py +0 -0
  60. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pyamdgpu/__init__.py +0 -0
  61. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pyamdsmi/__init__.py +0 -0
  62. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pycndev/__init__.py +0 -0
  63. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pyhgml/__init__.py +0 -0
  64. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pyhgml/libhgml.so +0 -0
  65. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pyhgml/libuki.so +0 -0
  66. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pyhsa/__init__.py +0 -0
  67. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pyixml/__init__.py +0 -0
  68. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pymtml/__init__.py +0 -0
  69. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pymxsml/__init__.py +0 -0
  70. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pynvml/__init__.py +0 -0
  71. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/pyrocmsmi/__init__.py +0 -0
  72. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/detector/thead.py +0 -0
  73. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/envs.py +0 -0
  74. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/gpustack_runtime/logging.py +0 -0
  75. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/hatch.toml +0 -0
  76. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/mkdocs.yml +0 -0
  77. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/pack/Dockerfile +0 -0
  78. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/pack/Dockerfile.dummy +0 -0
  79. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/pyproject.toml +0 -0
  80. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/pytest.ini +0 -0
  81. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/ruff.toml +0 -0
  82. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/specs/2026-08-14-detector-alignment-and-workload-exit-status.md +0 -0
  83. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/fixtures/__init__.py +0 -0
  84. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/fixtures/test_compare_versions.json +0 -0
  85. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/fixtures/test_correct_runner_image.json +0 -0
  86. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json.json +0 -0
  87. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_multiple_jsons.json +0 -0
  88. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_multiple_yamls.yaml +0 -0
  89. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_single_json.json +0 -0
  90. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json_single_yaml.yaml +0 -0
  91. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/fixtures/test_nginx_entrypoint.sh +0 -0
  92. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/test_docker_status.py +0 -0
  93. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/test_kubernetes_status.py +0 -0
  94. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/test_podman_status.py +0 -0
  95. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/test_privileged.py +0 -0
  96. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/test_resource_injection_policy.py +0 -0
  97. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/test_runtime_class.py +0 -0
  98. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/test_utils.py +0 -0
  99. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/test_visible_devices_ordering.py +0 -0
  100. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/deployer/test_workload_status.py +0 -0
  101. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/fixtures/__init__.py +0 -0
  102. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/README.md +0 -0
  103. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi300x.json +0 -0
  104. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi308x.json +0 -0
  105. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_amd_rx7800xt.json +0 -0
  106. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_ascend_310p3.json +0 -0
  107. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_ascend_910b2.json +0 -0
  108. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_hygon_k100ai.json +0 -0
  109. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_metax_c500.json +0 -0
  110. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_gb10.json +0 -0
  111. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100.json +0 -0
  112. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100_mig.json +0 -0
  113. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h200.json +0 -0
  114. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4080super.json +0 -0
  115. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090_48g.json +0 -0
  116. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090d.json +0 -0
  117. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx5090d.json +0 -0
  118. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/detect_output_thead_ppu.json +0 -0
  119. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi300x.json +0 -0
  120. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi308x.json +0 -0
  121. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_amd_rx7800xt.json +0 -0
  122. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_ascend_310p3.json +0 -0
  123. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_ascend_910b2.json +0 -0
  124. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_hygon_k100ai.json +0 -0
  125. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_metax_c500.json +0 -0
  126. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_mthreads_s5000.json +0 -0
  127. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100.json +0 -0
  128. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100_mig.json +0 -0
  129. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h200.json +0 -0
  130. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4080super.json +0 -0
  131. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090_48g.json +0 -0
  132. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090d.json +0 -0
  133. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx5090d.json +0 -0
  134. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/samples/topology_output_thead_ppu.json +0 -0
  135. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_amd.py +0 -0
  136. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_cambricon.py +0 -0
  137. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_detector_cli.py +0 -0
  138. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_detector_types.py +0 -0
  139. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_detector_utils.py +0 -0
  140. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_hygon.py +0 -0
  141. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_iluvatar.py +0 -0
  142. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_metax.py +0 -0
  143. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_mig_devices.py +0 -0
  144. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_mthreads.py +0 -0
  145. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_nvidia.py +0 -0
  146. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_pycndev.py +0 -0
  147. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_samples.py +0 -0
  148. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/tests/gpustack_runtime/detector/test_thead.py +0 -0
  149. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/uv.lock +0 -0
  150. {gpustack_runtime-0.2.3.post1 → gpustack_runtime-0.2.4}/uv.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gpustack-runtime
3
- Version: 0.2.3.post1
3
+ Version: 0.2.4
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.post1'
31
- __version_tuple__ = version_tuple = (0, 2, 3, 'post1')
30
+ __version__ = version = '0.2.4'
31
+ __version_tuple__ = version_tuple = (0, 2, 4)
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 = "59ea8c8"
@@ -362,6 +362,7 @@ class DeleteWorkloadSubCommand(SubCommand):
362
362
 
363
363
  namespace: str
364
364
  name: str
365
+ grace_period_seconds: int | None
365
366
 
366
367
  @staticmethod
367
368
  def register(parser: _SubParsersAction):
@@ -376,6 +377,13 @@ class DeleteWorkloadSubCommand(SubCommand):
376
377
  help="Namespace of the workload",
377
378
  )
378
379
 
380
+ delete_parser.add_argument(
381
+ "--grace-period-seconds",
382
+ type=int,
383
+ help="Duration in seconds the workload needs to terminate gracefully, "
384
+ "which overrides the one declared by the workload plan",
385
+ )
386
+
379
387
  delete_parser.add_argument(
380
388
  "name",
381
389
  type=str,
@@ -387,6 +395,7 @@ class DeleteWorkloadSubCommand(SubCommand):
387
395
  def __init__(self, args: Namespace):
388
396
  self.namespace = args.namespace
389
397
  self.name = args.name
398
+ self.grace_period_seconds = args.grace_period_seconds
390
399
 
391
400
  if not self.name:
392
401
  msg = "The name argument is required."
@@ -396,6 +405,7 @@ class DeleteWorkloadSubCommand(SubCommand):
396
405
  st = delete_workload(
397
406
  name=self.name,
398
407
  namespace=self.namespace,
408
+ grace_period_seconds=self.grace_period_seconds,
399
409
  )
400
410
  if st:
401
411
  print(f"Deleted workload '{self.name}'.")
@@ -428,11 +438,21 @@ class DeleteWorkloadsSubCommand(SubCommand):
428
438
  help="Filter workloads by labels (key=value pairs separated by commas)",
429
439
  )
430
440
 
441
+ delete_parser.add_argument(
442
+ "--grace-period-seconds",
443
+ type=int,
444
+ help="Duration in seconds the workloads need to terminate gracefully, "
445
+ "which overrides the one declared by the workload plans. "
446
+ "The workloads are deleted one after another, "
447
+ "so the whole deletion may take this duration per workload",
448
+ )
449
+
431
450
  delete_parser.set_defaults(func=DeleteWorkloadsSubCommand)
432
451
 
433
452
  def __init__(self, args: Namespace):
434
453
  self.namespace = args.namespace
435
454
  self.labels = args.labels
455
+ self.grace_period_seconds = args.grace_period_seconds
436
456
 
437
457
  def run(self):
438
458
  sts: list[WorkloadStatus] = list_workloads(
@@ -443,6 +463,7 @@ class DeleteWorkloadsSubCommand(SubCommand):
443
463
  delete_workload(
444
464
  name=st.name,
445
465
  namespace=st.namespace,
466
+ grace_period_seconds=self.grace_period_seconds,
446
467
  )
447
468
  print(f"Deleted workload '{st.name}'.")
448
469
  if not sts:
@@ -150,6 +150,7 @@ def get_workload(
150
150
  def delete_workload(
151
151
  name: WorkloadName,
152
152
  namespace: WorkloadNamespace | None = None,
153
+ grace_period_seconds: int | None = None,
153
154
  ) -> WorkloadStatus | None:
154
155
  """
155
156
  Delete the given workload.
@@ -159,11 +160,16 @@ def delete_workload(
159
160
  The name of the workload to delete.
160
161
  namespace:
161
162
  The namespace of the workload.
163
+ grace_period_seconds:
164
+ Duration in seconds the workload needs to terminate gracefully,
165
+ which overrides the one declared by the workload plan.
162
166
 
163
167
  Return:
164
168
  The status if found, None otherwise.
165
169
 
166
170
  Raises:
171
+ ValueError:
172
+ If the grace period is negative.
167
173
  UnsupportedError:
168
174
  If no deployer supports the given workload.
169
175
  OperationError:
@@ -174,7 +180,11 @@ def delete_workload(
174
180
  if not dep.is_supported():
175
181
  continue
176
182
 
177
- return dep.delete(name=name, namespace=namespace)
183
+ return dep.delete(
184
+ name=name,
185
+ namespace=namespace,
186
+ grace_period_seconds=grace_period_seconds,
187
+ )
178
188
 
179
189
  raise UnsupportedError(_NO_AVAILABLE_DEPLOYER_MSG)
180
190
 
@@ -832,6 +832,12 @@ WorkloadName = str
832
832
  Name for a workload.
833
833
  """
834
834
 
835
+ DEFAULT_TERMINATION_GRACE_PERIOD_SECONDS = 30
836
+ """
837
+ Default duration in seconds a workload needs to terminate gracefully,
838
+ which aligns with the Kubernetes API server default.
839
+ """
840
+
835
841
 
836
842
  @dataclass_json
837
843
  @dataclass
@@ -862,6 +868,8 @@ class WorkloadPlan(WorkloadSecurity):
862
868
  The group ID to own the filesystem of the workload.
863
869
  sysctls (dict[str, str] | None):
864
870
  Sysctls to set for the workload.
871
+ termination_grace_period_seconds (int):
872
+ Duration in seconds the containers of the workload need to terminate gracefully.
865
873
  containers (list[Container] | None):
866
874
  Containers in the workload.
867
875
  It must contain at least one "RUN" profile container.
@@ -909,6 +917,12 @@ class WorkloadPlan(WorkloadSecurity):
909
917
  Containers in the workload.
910
918
  It must contain at least one "RUN" profile container.
911
919
  """
920
+ termination_grace_period_seconds: int = DEFAULT_TERMINATION_GRACE_PERIOD_SECONDS
921
+ """
922
+ Duration in seconds the containers of the workload need to terminate gracefully.
923
+ Containers are signaled to stop, and killed once the duration elapses.
924
+ Zero means killing immediately.
925
+ """
912
926
 
913
927
  ##
914
928
  ## The below are internal use only fields.
@@ -957,6 +971,15 @@ class WorkloadPlan(WorkloadSecurity):
957
971
  msg = 'Workload must contain at least one "RUN" profile container.'
958
972
  raise ValueError(msg)
959
973
 
974
+ # Default and validate termination grace period.
975
+ if self.termination_grace_period_seconds is None:
976
+ self.termination_grace_period_seconds = (
977
+ DEFAULT_TERMINATION_GRACE_PERIOD_SECONDS
978
+ )
979
+ elif self.termination_grace_period_seconds < 0:
980
+ msg = "Workload termination grace period must not be negative."
981
+ raise ValueError(msg)
982
+
960
983
  # Validate workload labels, including label names and values.
961
984
  for ln, lv in self.labels.items():
962
985
  for s in ln.split("/"):
@@ -2109,6 +2132,8 @@ class Deployer(ABC):
2109
2132
  self,
2110
2133
  name: WorkloadName,
2111
2134
  namespace: WorkloadNamespace | None = None,
2135
+ *,
2136
+ grace_period_seconds: int | None = None,
2112
2137
  async_mode: bool | None = None,
2113
2138
  ) -> WorkloadStatus | None:
2114
2139
  """
@@ -2119,6 +2144,9 @@ class Deployer(ABC):
2119
2144
  The name of the workload.
2120
2145
  namespace:
2121
2146
  The namespace of the workload.
2147
+ grace_period_seconds:
2148
+ Duration in seconds the workload needs to terminate gracefully,
2149
+ which overrides the one declared by the workload plan.
2122
2150
  async_mode:
2123
2151
  Whether to execute in a separate thread.
2124
2152
 
@@ -2128,16 +2156,23 @@ class Deployer(ABC):
2128
2156
  Raises:
2129
2157
  UnsupportedError:
2130
2158
  If the deployer is not supported in the current environment.
2159
+ ValueError:
2160
+ If the grace period is negative.
2131
2161
  OperationError:
2132
2162
  If the workload fails to delete.
2133
2163
 
2134
2164
  """
2165
+ if grace_period_seconds is not None and grace_period_seconds < 0:
2166
+ msg = "Workload termination grace period must not be negative."
2167
+ raise ValueError(msg)
2168
+
2135
2169
  if async_mode:
2136
2170
  try:
2137
2171
  future = self.pool.submit(
2138
2172
  self._delete,
2139
2173
  name,
2140
2174
  namespace,
2175
+ grace_period_seconds,
2141
2176
  )
2142
2177
  return future.result()
2143
2178
  except OperationError:
@@ -2146,13 +2181,14 @@ class Deployer(ABC):
2146
2181
  msg = "Asynchronous workload delete failed."
2147
2182
  raise OperationError(msg) from e
2148
2183
  else:
2149
- return self._delete(name, namespace)
2184
+ return self._delete(name, namespace, grace_period_seconds)
2150
2185
 
2151
2186
  @abstractmethod
2152
2187
  def _delete(
2153
2188
  self,
2154
2189
  name: WorkloadName,
2155
2190
  namespace: WorkloadNamespace | None = None,
2191
+ grace_period_seconds: int | None = None,
2156
2192
  ) -> WorkloadStatus | None:
2157
2193
  """
2158
2194
  Delete a workload.
@@ -2162,6 +2198,9 @@ class Deployer(ABC):
2162
2198
  The name of the workload.
2163
2199
  namespace:
2164
2200
  The namespace of the workload.
2201
+ grace_period_seconds:
2202
+ Duration in seconds the workload needs to terminate gracefully,
2203
+ which overrides the one declared by the workload plan.
2165
2204
 
2166
2205
  Return:
2167
2206
  The status if found, None otherwise.
@@ -143,6 +143,56 @@ def device_to_cdi_device_node(
143
143
  )
144
144
 
145
145
 
146
+ def path_to_cdi_device_nodes(
147
+ path: str,
148
+ permission: str = "rw",
149
+ no_user: bool = False,
150
+ ) -> list[ConfigDeviceNode]:
151
+ """
152
+ Convert a device path, or a directory holding device paths, to ConfigDeviceNodes.
153
+
154
+ A generation may expose a bus as a directory of device nodes rather than as
155
+ a single node -- Ascend's UB does, which is why the operator enumerates it,
156
+ see addUBDevicesFromDir in
157
+ https://gitcode.com/Ascend/mind-cluster/blob/master/component/ascend-common/cdi/devnode.go.
158
+ Both shapes are accepted so that a path which is a plain device node on one
159
+ driver and a directory on another needs no caller-side branch.
160
+
161
+ Args:
162
+ path:
163
+ Path to the device, or to a directory of devices, on the host.
164
+ permission:
165
+ Permissions for the devices.
166
+ no_user:
167
+ Whether to omit user and group information.
168
+
169
+ Returns:
170
+ The ConfigDeviceNode objects, empty if the path holds no device.
171
+
172
+ """
173
+ p = Path(path)
174
+ if not p.is_dir():
175
+ cdn = device_to_cdi_device_node(
176
+ path=path,
177
+ permission=permission,
178
+ no_user=no_user,
179
+ )
180
+ return [cdn] if cdn else []
181
+
182
+ return [
183
+ cdn
184
+ for entry in sorted(p.iterdir())
185
+ if not entry.is_dir()
186
+ and (
187
+ cdn := device_to_cdi_device_node(
188
+ path=str(entry),
189
+ permission=permission,
190
+ no_user=no_user,
191
+ )
192
+ )
193
+ ]
194
+
195
+
146
196
  def path_to_cdi_mount(
147
197
  path: str,
148
198
  container_path: str | None = None,
@@ -181,3 +231,34 @@ def path_to_cdi_mount(
181
231
  container_path=container_path,
182
232
  options=options,
183
233
  )
234
+
235
+
236
+ def glob_to_cdi_mounts(
237
+ pattern: str,
238
+ options: list[str] | None = None,
239
+ ) -> list[ConfigMount]:
240
+ """
241
+ Convert every path matching a glob pattern to ConfigMounts.
242
+
243
+ A user-space library is versioned in its file name, so the set of files to
244
+ mount cannot be spelled out ahead of time -- the operator's mount profile
245
+ lists them as patterns for the same reason.
246
+
247
+ Args:
248
+ pattern:
249
+ Path on the host whose last segment may carry a wildcard; the
250
+ directory part is taken literally, so a wildcard there matches
251
+ nothing. A pattern without a wildcard resolves to that path alone.
252
+ options:
253
+ Mount options.
254
+
255
+ Returns:
256
+ The ConfigMount objects, empty if nothing matches.
257
+
258
+ """
259
+ p = Path(pattern)
260
+ return [
261
+ cm
262
+ for path in sorted(p.parent.glob(p.name))
263
+ if (cm := path_to_cdi_mount(path=str(path), options=options))
264
+ ]
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations as __future_annotations__
2
2
 
3
+ import logging
4
+
3
5
  from ...detector import (
4
6
  Devices,
5
7
  ManufacturerEnum,
6
8
  detect_devices,
7
9
  filter_devices_by_manufacturer,
8
10
  )
11
+ from ...detector.ascend import get_ascend_cann_variant
9
12
  from .__types__ import (
10
13
  Config,
11
14
  ConfigContainerEdits,
@@ -14,7 +17,44 @@ from .__types__ import (
14
17
  manufacturer_to_cdi_kind,
15
18
  manufacturer_to_runtime_env,
16
19
  )
17
- from .__utils__ import device_to_cdi_device_node, path_to_cdi_mount
20
+ from .__utils__ import (
21
+ device_to_cdi_device_node,
22
+ glob_to_cdi_mounts,
23
+ path_to_cdi_device_nodes,
24
+ path_to_cdi_mount,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ _A5_CANN_VARIANT = "950"
30
+ """
31
+ The CANN variant of the A5 generation, whose UB fabric replaces what the
32
+ earlier generations reach through the shared memory device.
33
+ """
34
+
35
+ _A5_UB_MOUNT_PATTERNS = [
36
+ "/usr/lib64/libummu*",
37
+ "/usr/lib64/liburma*",
38
+ "/usr/lib64/urma",
39
+ "/usr/lib64/libnl*",
40
+ "/usr/bin/urma_admin",
41
+ "/usr/bin/urma_perftest",
42
+ "/usr/bin/urma_ping",
43
+ ]
44
+ """
45
+ The UB user-space libraries an A5 container needs, mirroring the operator's
46
+ mount profile for the Ascend950 generation, see
47
+ https://gitcode.com/Ascend/mind-cluster/blob/master/component/ascend-common/cdi/mount/profile.go.
48
+
49
+ These are mounted for the A5 generation only. Some of them -- libnl above all
50
+ -- are ordinary system libraries present on any host, so mounting them
51
+ unconditionally would shadow what an earlier generation's container ships with
52
+ its own image.
53
+
54
+ The /usr/lib64 prefix is the operator's, and holds on the openEuler and CentOS
55
+ hosts an A5 ships on; a Debian-derived host uses /usr/lib/<triplet>, where none
56
+ of these match -- hence the log line when a pattern finds nothing.
57
+ """
18
58
 
19
59
 
20
60
  class AscendGenerator(Generator):
@@ -73,16 +113,14 @@ class AscendGenerator(Generator):
73
113
  break
74
114
  for p in [
75
115
  "/dev/dvpp_cmdlist",
116
+ # UB exposes a directory of device nodes rather than a single one,
117
+ # so every entry below it has to be injected.
76
118
  "/dev/uburma",
77
119
  "/dev/ummu",
78
120
  "/dev/devmm_svm",
79
121
  "/dev/hisi_hdc",
80
122
  ]:
81
- cdn = device_to_cdi_device_node(
82
- path=p,
83
- )
84
- if cdn:
85
- common_device_nodes.append(cdn)
123
+ common_device_nodes.extend(path_to_cdi_device_nodes(path=p))
86
124
  if not common_device_nodes:
87
125
  return None
88
126
 
@@ -102,6 +140,25 @@ class AscendGenerator(Generator):
102
140
  if cm:
103
141
  common_mounts.append(cm)
104
142
 
143
+ # Device.appendix defaults to None and callers pass their own devices
144
+ # in, so every read goes through `or {}`.
145
+ if any(
146
+ get_ascend_cann_variant((dev.appendix or {}).get("arch_family"))
147
+ == _A5_CANN_VARIANT
148
+ for dev in devices
149
+ if dev
150
+ ):
151
+ for pattern in _A5_UB_MOUNT_PATTERNS:
152
+ ub_mounts = glob_to_cdi_mounts(pattern=pattern)
153
+ if not ub_mounts:
154
+ # Otherwise this surfaces later as a container that cannot
155
+ # reach the UB fabric.
156
+ logger.debug(
157
+ "No UB library matched %s, an A5 container will lack it",
158
+ pattern,
159
+ )
160
+ common_mounts.extend(ub_mounts)
161
+
105
162
  cdi_devices: list[ConfigDevice] = []
106
163
 
107
164
  all_device_nodes = []
@@ -119,7 +176,7 @@ class AscendGenerator(Generator):
119
176
  # addressed by the index, which would resolve to another NPU's
120
177
  # node. The detector already drops such a device; this guards the
121
178
  # devices a caller passes in.
122
- cdn_number = dev.appendix.get("physical_id")
179
+ cdn_number = (dev.appendix or {}).get("physical_id")
123
180
  if cdn_number is None:
124
181
  continue
125
182
  cdn_path = f"/dev/davinci{cdn_number}"
@@ -8,6 +8,7 @@ import os
8
8
  import socket
9
9
  import sys
10
10
  import tarfile
11
+ from concurrent.futures import ThreadPoolExecutor
11
12
  from dataclasses import dataclass, field
12
13
  from math import ceil
13
14
  from pathlib import Path
@@ -27,6 +28,7 @@ from tqdm import tqdm
27
28
  from .. import envs
28
29
  from ..logging import debug_log_exception, debug_log_warning
29
30
  from .__types__ import (
31
+ DEFAULT_TERMINATION_GRACE_PERIOD_SECONDS,
30
32
  Container,
31
33
  ContainerCheck,
32
34
  ContainerEnv,
@@ -68,6 +70,9 @@ _LABEL_COMPONENT = f"{envs.GPUSTACK_RUNTIME_DEPLOY_LABEL_PREFIX}/component"
68
70
  _LABEL_COMPONENT_NAME = f"{_LABEL_COMPONENT}-name"
69
71
  _LABEL_COMPONENT_INDEX = f"{_LABEL_COMPONENT}-index"
70
72
  _LABEL_COMPONENT_HEAL_PREFIX = f"{_LABEL_COMPONENT}-heal"
73
+ _LABEL_TERMINATION_GRACE_PERIOD_SECONDS = (
74
+ f"{envs.GPUSTACK_RUNTIME_DEPLOY_LABEL_PREFIX}/termination-grace-period-seconds"
75
+ )
71
76
 
72
77
 
73
78
  @dataclass_json
@@ -104,6 +109,8 @@ class DockerWorkloadPlan(WorkloadPlan):
104
109
  The group ID to own the filesystem of the workload.
105
110
  sysctls (dict[str, str] | None):
106
111
  Sysctls to set for the workload.
112
+ termination_grace_period_seconds (int):
113
+ Duration in seconds the containers of the workload need to terminate gracefully.
107
114
  containers (list[tuple[int, Container]] | None):
108
115
  List of containers in the workload.
109
116
  It must contain at least one "RUN" profile container.
@@ -138,6 +145,12 @@ class DockerWorkloadPlan(WorkloadPlan):
138
145
  # Default and validate in the base class.
139
146
  super().validate_and_default()
140
147
 
148
+ # Carry the termination grace period on the containers,
149
+ # as the Docker models layer cannot express a create time stop timeout.
150
+ self.labels[_LABEL_TERMINATION_GRACE_PERIOD_SECONDS] = str(
151
+ self.termination_grace_period_seconds,
152
+ )
153
+
141
154
  # Adjust images.
142
155
  self.pause_image = adjust_image_with_envs(self.pause_image)
143
156
  self.unhealthy_restart_image = adjust_image_with_envs(
@@ -1650,6 +1663,7 @@ class DockerDeployer(EndoscopicDeployer):
1650
1663
  self,
1651
1664
  name: WorkloadName,
1652
1665
  namespace: WorkloadNamespace | None = None,
1666
+ grace_period_seconds: int | None = None,
1653
1667
  ) -> WorkloadStatus | None:
1654
1668
  """
1655
1669
  Delete a Docker workload.
@@ -1659,6 +1673,9 @@ class DockerDeployer(EndoscopicDeployer):
1659
1673
  The name of the workload.
1660
1674
  namespace:
1661
1675
  The namespace of the workload.
1676
+ grace_period_seconds:
1677
+ Duration in seconds the workload needs to terminate gracefully,
1678
+ which overrides the one declared by the workload plan.
1662
1679
 
1663
1680
  Return:
1664
1681
  The status if found, None otherwise.
@@ -1678,19 +1695,40 @@ class DockerDeployer(EndoscopicDeployer):
1678
1695
  # Remove all containers with the workload label.
1679
1696
  try:
1680
1697
  d_containers = getattr(workload, "_d_containers", [])
1681
- # Remove non-pause containers first.
1698
+ # Remove the unhealthy restart container first,
1699
+ # otherwise it restarts the containers draining below.
1682
1700
  for c in d_containers:
1683
- if "-pause" not in c.name:
1701
+ if c.labels.get(_LABEL_COMPONENT) == "unhealthy-restart":
1684
1702
  c.remove(
1685
1703
  force=True,
1686
1704
  )
1687
- # Then remove pause containers.
1705
+ # Then drain the non-pause containers concurrently,
1706
+ # so each of them gets the whole grace period,
1707
+ # and remove them.
1708
+ d_drain_containers = [
1709
+ c
1710
+ for c in d_containers
1711
+ if c.labels.get(_LABEL_COMPONENT) not in ("pause", "unhealthy-restart")
1712
+ ]
1713
+ _stop_containers(
1714
+ d_drain_containers,
1715
+ _termination_grace_period_seconds(d_containers, grace_period_seconds),
1716
+ )
1717
+ for c in d_drain_containers:
1718
+ c.remove(
1719
+ force=True,
1720
+ )
1721
+ # Finally remove the pause containers,
1722
+ # which hold the namespaces shared by the above containers.
1688
1723
  for c in d_containers:
1689
- if "-pause" in c.name:
1724
+ if c.labels.get(_LABEL_COMPONENT) == "pause":
1690
1725
  c.remove(
1691
1726
  force=True,
1692
1727
  )
1693
- except docker.errors.APIError as e:
1728
+ # NB(thxCode): Deleting a workload always fails with an OperationError,
1729
+ # as neither the Docker SDK errors nor the transport ones underneath them
1730
+ # share a base class narrow enough to catch on its own.
1731
+ except Exception as e:
1694
1732
  msg = f"Failed to delete containers for workload {name}{_detail_api_call_error(e)}"
1695
1733
  raise OperationError(msg) from e
1696
1734
 
@@ -1711,7 +1749,7 @@ class DockerDeployer(EndoscopicDeployer):
1711
1749
  v.remove(
1712
1750
  force=True,
1713
1751
  )
1714
- except docker.errors.APIError as e:
1752
+ except Exception as e:
1715
1753
  msg = f"Failed to delete volumes for workload {name}{_detail_api_call_error(e)}"
1716
1754
  raise OperationError(msg) from e
1717
1755
 
@@ -2168,6 +2206,71 @@ def _has_restart_policy(
2168
2206
  )
2169
2207
 
2170
2208
 
2209
+ def _stop_containers(
2210
+ containers: list[docker.models.containers.Container],
2211
+ grace_period_seconds: int,
2212
+ ):
2213
+ """
2214
+ Stop the given containers concurrently,
2215
+ so each of them gets the whole grace period,
2216
+ which mirrors how Kubernetes terminates the containers of a Pod.
2217
+
2218
+ A container failing to stop is left to the forceful removal following this,
2219
+ as the removal is the authoritative teardown.
2220
+
2221
+ Args:
2222
+ containers:
2223
+ List of Docker containers to stop.
2224
+ grace_period_seconds:
2225
+ Duration in seconds the containers need to terminate gracefully.
2226
+
2227
+ """
2228
+ if not containers:
2229
+ return
2230
+
2231
+ def stop(container: docker.models.containers.Container):
2232
+ try:
2233
+ container.stop(
2234
+ timeout=grace_period_seconds,
2235
+ )
2236
+ except Exception:
2237
+ debug_log_exception(logger, f"Failed to stop container {container.name}")
2238
+
2239
+ with ThreadPoolExecutor(max_workers=min(len(containers), 16)) as pool:
2240
+ list(pool.map(stop, containers))
2241
+
2242
+
2243
+ def _termination_grace_period_seconds(
2244
+ containers: list[docker.models.containers.Container],
2245
+ override: int | None = None,
2246
+ ) -> int:
2247
+ """
2248
+ Resolve how long the given containers may take to terminate gracefully,
2249
+ which prefers the given override, then the one declared by the workload plan,
2250
+ and finally the default.
2251
+
2252
+ Args:
2253
+ containers:
2254
+ List of Docker containers in the workload.
2255
+ override:
2256
+ Duration in seconds overriding the one declared by the workload plan.
2257
+
2258
+ Returns:
2259
+ The duration in seconds.
2260
+
2261
+ """
2262
+ if override is not None:
2263
+ return override
2264
+
2265
+ for c in containers:
2266
+ declared = c.labels.get(_LABEL_TERMINATION_GRACE_PERIOD_SECONDS)
2267
+ if declared:
2268
+ with contextlib.suppress(ValueError):
2269
+ return max(0, int(declared))
2270
+
2271
+ return DEFAULT_TERMINATION_GRACE_PERIOD_SECONDS
2272
+
2273
+
2171
2274
  class DockerWorkloadExecStream(WorkloadExecStream):
2172
2275
  """
2173
2276
  A WorkloadExecStream implementation for Docker exec socket streams.
@@ -2202,14 +2305,14 @@ class DockerWorkloadExecStream(WorkloadExecStream):
2202
2305
  self._sock.close()
2203
2306
 
2204
2307
 
2205
- def _detail_api_call_error(err: docker.errors.APIError) -> str:
2308
+ def _detail_api_call_error(err: Exception) -> str:
2206
2309
  """
2207
2310
  Explain a Docker API error in a concise way,
2208
2311
  if the envs.GPUSTACK_RUNTIME_DEPLOY_API_CALL_ERROR_DETAIL is enabled.
2209
2312
 
2210
2313
  Args:
2211
2314
  err:
2212
- The Docker API error.
2315
+ The Docker API error, or the transport error underneath it.
2213
2316
 
2214
2317
  Returns:
2215
2318
  A concise explanation of the error.
@@ -2218,6 +2321,9 @@ def _detail_api_call_error(err: docker.errors.APIError) -> str:
2218
2321
  if not envs.GPUSTACK_RUNTIME_DEPLOY_API_CALL_ERROR_DETAIL:
2219
2322
  return ""
2220
2323
 
2324
+ if not isinstance(err, docker.errors.APIError):
2325
+ return f": {err}"
2326
+
2221
2327
  msg = f": Docker {'Client' if err.is_client_error() else 'Server'} Error"
2222
2328
  if err.explanation:
2223
2329
  msg += f": {err.explanation}"