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.
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/PKG-INFO +1 -1
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/_version.py +2 -2
- gpustack_runtime-0.2.3.post1/gpustack_runtime/_version_appendix.py +1 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/__types__.py +30 -1
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/docker.py +11 -5
- gpustack_runtime-0.2.3.post1/gpustack_runtime/deployer/k8s/devicemanager/__init__.py +149 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/kuberentes.py +95 -12
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/podman.py +11 -5
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_privileged.py +2 -6
- gpustack_runtime-0.2.3.post1/tests/gpustack_runtime/deployer/test_resource_injection_policy.py +208 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_visible_devices_ordering.py +32 -1
- gpustack_runtime-0.2.3/gpustack_runtime/_version_appendix.py +0 -1
- gpustack_runtime-0.2.3/gpustack_runtime/deployer/k8s/devicemanager/__init__.py +0 -78
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.codespelldict +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.codespellrc +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.dockerignore +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.gitattributes +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.gitignore +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.pre-commit-config.yaml +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/.python-version +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/LICENSE +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/Makefile +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/README.md +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/deploy/manifests/docker-compose.yaml +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/deploy/manifests/kubernetes.yaml +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/docs/index.md +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/docs/modules/gpustack_runtime.deployer.md +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/docs/modules/gpustack_runtime.detector.md +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/docs/modules/gpustack_runtime.md +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/__main__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/_version.pyi +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/__types__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/deployer.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/detector.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/cmds/images.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/__patches__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/__utils__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/__types__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/__utils__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/amd.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/ascend.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/hygon.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/iluvatar.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/metax.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/cdi/thead.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/__types__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/__utils__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/amd.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/ascend.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/cambricon.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/hygon.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/iluvatar.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/metax.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/mthreads.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/nvidia.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyamdgpu/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyamdsmi/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pycndev/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pydcmi/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyhgml/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyhgml/libhgml.so +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyhgml/libuki.so +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyhsa/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyixml/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pymtml/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pymxsml/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pynvml/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/pyrocmsmi/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/detector/thead.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/envs.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/logging.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/hatch.toml +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/mkdocs.yml +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/pack/Dockerfile +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/pack/Dockerfile.dummy +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/pyproject.toml +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/pytest.ini +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/ruff.toml +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/specs/2026-08-14-detector-alignment-and-workload-exit-status.md +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_compare_versions.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_correct_runner_image.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_load_yaml_or_json.json +0 -0
- {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
- {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
- {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
- {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
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/fixtures/test_nginx_entrypoint.sh +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_docker_status.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_kubernetes_status.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_podman_status.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_runtime_class.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_utils.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/deployer/test_workload_status.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/fixtures/__init__.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/README.md +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi300x.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi308x.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_amd_rx7800xt.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_ascend_310p3.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_ascend_910b2.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_hygon_k100ai.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_metax_c500.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_gb10.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100_mig.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h200.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4080super.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090_48g.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090d.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx5090d.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/detect_output_thead_ppu.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi300x.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi308x.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_amd_rx7800xt.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_ascend_310p3.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_ascend_910b2.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_hygon_k100ai.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_metax_c500.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_mthreads_s5000.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100_mig.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h200.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4080super.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090_48g.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090d.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx5090d.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/samples/topology_output_thead_ppu.json +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_amd.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_ascend.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_cambricon.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_detector_cli.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_detector_types.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_detector_utils.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_hygon.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_iluvatar.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_metax.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_mig_devices.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_mthreads.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_nvidia.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_pycndev.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_samples.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/tests/gpustack_runtime/detector/test_thead.py +0 -0
- {gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/uv.lock +0 -0
- {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"
|
{gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/__types__.py
RENAMED
|
@@ -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
|
|
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
|
-
#
|
|
1122
|
-
#
|
|
1123
|
-
#
|
|
1124
|
-
#
|
|
1125
|
-
|
|
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
|
+
]
|
{gpustack_runtime-0.2.3 → gpustack_runtime-0.2.3.post1}/gpustack_runtime/deployer/kuberentes.py
RENAMED
|
@@ -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
|
-
#
|
|
1550
|
-
#
|
|
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
|
-
#
|
|
1554
|
-
#
|
|
1555
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
1101
|
-
#
|
|
1102
|
-
#
|
|
1103
|
-
#
|
|
1104
|
-
|
|
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
|
|
132
|
-
|
|
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}"
|
gpustack_runtime-0.2.3.post1/tests/gpustack_runtime/deployer/test_resource_injection_policy.py
ADDED
|
@@ -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
|