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