rexprun 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rexprun-0.2.0 → rexprun-0.2.2}/MANIFEST.in +1 -0
- {rexprun-0.2.0/rexprun.egg-info → rexprun-0.2.2}/PKG-INFO +1 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/pyproject.toml +1 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/main.py +7 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/README.md +1 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/README.md +1 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune/README.md +1 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune/experiment/trainable.py +5 -2
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune_npu/README.md +1 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/accelerators.py +41 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/provenance.py +8 -2
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/state_store.py +2 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/docs/schemas.md +2 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/docs//344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +14 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/environments.py +69 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/node.py +19 -9
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/runner.py +10 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/scheduler.py +6 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/services.py +5 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/tuning.py +21 -8
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/skills/experiment-authoring/SKILL.md +3 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/skills/platform-operations/SKILL.md +16 -0
- rexprun-0.2.2/rexp/version.py +1 -0
- {rexprun-0.2.0 → rexprun-0.2.2/rexprun.egg-info}/PKG-INFO +1 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/SOURCES.txt +1 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_environment_setup.py +21 -0
- rexprun-0.2.2/tests/test_first_use_regressions.py +117 -0
- rexprun-0.2.0/rexp/version.py +0 -1
- {rexprun-0.2.0 → rexprun-0.2.2}/LICENSE +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/README.md +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/__init__.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/__main__.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/__init__.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ascend_smoke/README.md +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ascend_smoke/configs/smoke.yaml +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ascend_smoke/experiment/experiment.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/analysis/analyze.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/configs/full.yaml +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/configs/smoke.yaml +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/evaluation/summarize.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/experiment/README.md +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/experiment/experiment.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/experiment/experiment_helpers.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/tests/test_experiment.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/configs/full.yaml +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/configs/smoke.yaml +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/evaluation/kv_error.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/experiment/kv_staleness.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune/configs/smoke.yaml +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune_npu/configs/smoke.yaml +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune_npu/experiment/trainable.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/__init__.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/conda.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/config.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/machine.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/paths.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/process.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/resources.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/run_lock.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/schemas.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/states.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/storage.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/tuning.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/datasets/__init__.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/datasets/base.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/datasets/huggingface.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/datasets/local_jsonl.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/models/__init__.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/models/base.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/models/dream.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/models/llada.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/__init__.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/analysis.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/assets.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/dependencies.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/envprobe.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/evaluation.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/mcp.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/run_cleanup.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/script_execution.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/script_worker.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/setup.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/trial_queue.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/dependency_links.txt +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/entry_points.txt +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/requires.txt +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/top_level.txt +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/setup.cfg +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_adapters.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_ascend.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_assets.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_bundled_resources.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_cli.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_command_audit.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_crash_recovery.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_env_selection.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_environment_catalog.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_experiment_loading.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_initial_setup.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_lifecycle.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_network_config.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_pause_resume.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_platform_info.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_plugin_interfaces.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_process_linux.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_project_deps.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_research_catalog.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_retry.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_run_cleanup.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_run_metadata.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_runs_limit.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_scheduler_checks.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_scheduler_gpu.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_script_environment.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_skills_bundled.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_startup_resume.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_storage.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_trial_queue.py +0 -0
- {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_tuning.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rexprun
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Research experiment orchestration with resource-aware scheduling, isolated environments, checkpoint recovery, and Ray Tune searches on GPUs and Ascend NPUs.
|
|
5
5
|
Author: REXP contributors
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rexprun"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Research experiment orchestration with resource-aware scheduling, isolated environments, checkpoint recovery, and Ray Tune searches on GPUs and Ascend NPUs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -284,7 +284,7 @@ def _gpu_summary(node: dict) -> str:
|
|
|
284
284
|
return f"{sum(names.values())} {_accelerator_label(node)}s ({models})"
|
|
285
285
|
count = node.get("gpu_count") or 0
|
|
286
286
|
if not count:
|
|
287
|
-
return "none"
|
|
287
|
+
return "probe failed; run rexp doctor" if node.get("accelerator_probe_error") else "none"
|
|
288
288
|
name = node.get("gpu_name") or "unknown model"
|
|
289
289
|
return f"{count} × {name}"
|
|
290
290
|
|
|
@@ -478,6 +478,10 @@ def init(
|
|
|
478
478
|
env_setup.prepare_runtime(paths, manager)
|
|
479
479
|
node.environment_manager = manager
|
|
480
480
|
save_nodes_config(paths, node)
|
|
481
|
+
command = env_setup.register_command(manager.runtime_python)
|
|
482
|
+
console.print(f"Global command: {command['path']}")
|
|
483
|
+
if command.get("new_terminal_required"):
|
|
484
|
+
console.print("Open a new terminal to load the updated PATH; the absolute command above works now.")
|
|
481
485
|
console.print(f"Platform runtime: {manager.runtime_python}")
|
|
482
486
|
console.print(f"Managed experiment directory: {paths.root / 'env'}")
|
|
483
487
|
console.print("Preparing default experiment environment...")
|
|
@@ -536,6 +540,8 @@ def doctor(json_output: bool = typer.Option(False, "--json")):
|
|
|
536
540
|
):
|
|
537
541
|
table.add_row(item, str(value))
|
|
538
542
|
console.print(table)
|
|
543
|
+
if hardware.get("accelerator_probe_error"):
|
|
544
|
+
console.print(f"Accelerator probe failed: {hardware['accelerator_probe_error']}", markup=False)
|
|
539
545
|
_print_environment_report(info["environments"])
|
|
540
546
|
_print_default_env_note(info.get("default_experiment_environment"))
|
|
541
547
|
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
## 使用
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
rexp submit {{project}} {{version}} --config configs/smoke.yaml
|
|
54
|
+
rexp submit {{project}} {{version}} --config <PLATFORM_ROOT>/research/{{project}}/{{version}}/configs/smoke.yaml
|
|
55
55
|
rexp start
|
|
56
56
|
rexp status
|
|
57
57
|
rexp run eval <run_id>
|
|
@@ -16,7 +16,7 @@ Diffusion steps 之间的 KV state 是否存在 temporal locality / sparsity?
|
|
|
16
16
|
|
|
17
17
|
1. 确认 CUDA 环境(`rexp doctor`,环境需 cu128 wheel 的 PyTorch)。
|
|
18
18
|
2. 把 LLaDA checkpoint 放到 `PLATFORM_ROOT/models_store/llada`。
|
|
19
|
-
3. `rexp submit {{project}} {{version}} --config configs/smoke.yaml`
|
|
19
|
+
3. `rexp submit {{project}} {{version}} --config <PLATFORM_ROOT>/research/{{project}}/{{version}}/configs/smoke.yaml`
|
|
20
20
|
4. `rexp start` / `rexp status`
|
|
21
21
|
5. Smoke 通过后跑 `configs/full.yaml`。
|
|
22
22
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
登记该环境并在配置中填写 `experiment.environment`,然后:
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
|
-
rexp submit {{project}} {{version}} --config configs/smoke.yaml
|
|
7
|
+
rexp submit {{project}} {{version}} --config <PLATFORM_ROOT>/research/{{project}}/{{version}}/configs/smoke.yaml
|
|
8
8
|
rexp start
|
|
9
9
|
rexp tune list
|
|
10
10
|
rexp tune show run_001
|
|
@@ -20,6 +20,9 @@ def trainable(config):
|
|
|
20
20
|
state = artifacts / 'checkpoint-staging'
|
|
21
21
|
state.mkdir(exist_ok=True)
|
|
22
22
|
(state / 'state.json').write_text(json.dumps({'step': step + 1}))
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
metrics = {'score': score, 'samples_completed': step + 1}
|
|
24
|
+
if step + 1 == 4:
|
|
25
|
+
tune.report(metrics, checkpoint=tune.Checkpoint.from_directory(str(state)))
|
|
26
|
+
else:
|
|
27
|
+
tune.report(metrics)
|
|
25
28
|
(artifacts / 'result.json').write_text(json.dumps({'x': config['x'], 'score': score}))
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
此示例执行真实 NPU 标量运算和梯度校验,搜索 x=2。`resources.gpu_count: 2` 是搜索池的两个**逻辑设备**;A3 一张物理卡可能正好暴露这两个设备。`tune.gpus_per_trial: 1` 表示每个尝试使用一个逻辑 NPU,内部使用 `npu:0`。单逻辑设备实例将池、并发都改成 1。
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
rexp submit {{project}} {{version}} --config configs/smoke.yaml
|
|
8
|
+
rexp submit {{project}} {{version}} --config <PLATFORM_ROOT>/research/{{project}}/{{version}}/configs/smoke.yaml
|
|
9
9
|
rexp start
|
|
10
10
|
rexp tune show run_001
|
|
11
11
|
```
|
|
@@ -6,9 +6,50 @@ Importing this module does not import PyTorch or initialize a device runtime.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
from pathlib import Path
|
|
9
12
|
import re
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
def ascend_runtime_environment(inherited=None):
|
|
16
|
+
"""Use installed vendor libraries for non-login SSH, without running shell profiles.
|
|
17
|
+
|
|
18
|
+
Preserve user-supplied paths and container masks. Only existing standard
|
|
19
|
+
installation directories are appended; no driver/framework installation.
|
|
20
|
+
"""
|
|
21
|
+
env = dict(os.environ if inherited is None else inherited)
|
|
22
|
+
root = Path('/usr/local/Ascend')
|
|
23
|
+
# CANN 9 images use cann/; older installations use ascend-toolkit/latest.
|
|
24
|
+
# Select one installation, preserving an explicitly configured version.
|
|
25
|
+
toolkit = next((path for path in [
|
|
26
|
+
Path(env[key]) for key in ('ASCEND_HOME_PATH', 'ASCEND_TOOLKIT_HOME') if env.get(key)
|
|
27
|
+
] + [root / 'cann', root / 'ascend-toolkit/latest'] if path.is_dir()), None)
|
|
28
|
+
paths = {
|
|
29
|
+
'PATH': [root / 'driver/tools', root / 'driver/bin'],
|
|
30
|
+
'LD_LIBRARY_PATH': [root / 'driver/lib64', root / 'driver/lib64/common',
|
|
31
|
+
root / 'driver/lib64/driver'],
|
|
32
|
+
}
|
|
33
|
+
if toolkit:
|
|
34
|
+
paths['PATH'] += [toolkit / 'bin', toolkit / 'tools/ccec_compiler/bin']
|
|
35
|
+
paths['LD_LIBRARY_PATH'] += [toolkit / 'lib64', toolkit / 'lib64/plugin/opskernel',
|
|
36
|
+
toolkit / 'lib64/plugin/nnengine',
|
|
37
|
+
toolkit / f'opp/built-in/op_impl/ai_core/tbe/op_tiling/lib/linux/{platform.machine()}']
|
|
38
|
+
paths['PYTHONPATH'] = [toolkit / 'python/site-packages', toolkit / 'opp/built-in/op_impl/ai_core/tbe']
|
|
39
|
+
for key, path in {'ASCEND_HOME_PATH': toolkit, 'ASCEND_TOOLKIT_HOME': toolkit,
|
|
40
|
+
'ASCEND_OPP_PATH': toolkit / 'opp', 'ASCEND_AICPU_PATH': toolkit}.items():
|
|
41
|
+
if path.is_dir():
|
|
42
|
+
env.setdefault(key, str(path))
|
|
43
|
+
for key, candidates in paths.items():
|
|
44
|
+
values = [v for v in env.get(key, '').split(os.pathsep) if v]
|
|
45
|
+
for path in candidates:
|
|
46
|
+
if path.is_dir() and str(path) not in values:
|
|
47
|
+
values.append(str(path))
|
|
48
|
+
if values:
|
|
49
|
+
env[key] = os.pathsep.join(values)
|
|
50
|
+
return env
|
|
51
|
+
|
|
52
|
+
|
|
12
53
|
def ascend_visible_ids(mask: str | None) -> set[int] | None:
|
|
13
54
|
"""Accept complete, increasing runtime masks; malformed masks expose nothing.
|
|
14
55
|
|
|
@@ -14,6 +14,8 @@ from rexp.core.storage import write_json, write_text_atomic
|
|
|
14
14
|
NPU_PROBE = r"""
|
|
15
15
|
out.update(torch_npu=None, npu_available=False, npu_device_count=0, npu_error=None)
|
|
16
16
|
try:
|
|
17
|
+
if out.get("torch_error"):
|
|
18
|
+
raise RuntimeError(out["torch_error"])
|
|
17
19
|
import torch_npu
|
|
18
20
|
import torch
|
|
19
21
|
out["torch_npu"] = getattr(torch_npu, "__version__", "unknown")
|
|
@@ -36,8 +38,9 @@ try:
|
|
|
36
38
|
out["torch"] = torch.__version__
|
|
37
39
|
out["cuda"] = torch.version.cuda
|
|
38
40
|
out["cuda_available"] = bool(torch.cuda.is_available())
|
|
39
|
-
except Exception:
|
|
41
|
+
except Exception as exc:
|
|
40
42
|
out["torch"] = None
|
|
43
|
+
out["torch_error"] = type(exc).__name__ + ": " + str(exc)
|
|
41
44
|
out["cuda"] = None
|
|
42
45
|
out["cuda_available"] = False
|
|
43
46
|
""" + NPU_PROBE + r"""
|
|
@@ -99,12 +102,14 @@ def get_git_commit(repo: Optional[str | Path]) -> Optional[str]:
|
|
|
99
102
|
def environment_snapshot(python_executable: Optional[str] = None) -> dict:
|
|
100
103
|
"""Probe a python interpreter (subprocess) or the current one."""
|
|
101
104
|
if python_executable:
|
|
105
|
+
from rexp.core.accelerators import ascend_runtime_environment
|
|
102
106
|
try:
|
|
103
107
|
out = subprocess.run(
|
|
104
108
|
[str(python_executable), "-c", ENV_PROBE],
|
|
105
109
|
capture_output=True,
|
|
106
110
|
text=True,
|
|
107
111
|
timeout=180,
|
|
112
|
+
env=ascend_runtime_environment(),
|
|
108
113
|
)
|
|
109
114
|
if out.returncode == 0:
|
|
110
115
|
lines = [ln for ln in out.stdout.strip().splitlines() if ln.strip()]
|
|
@@ -121,8 +126,9 @@ def environment_snapshot(python_executable: Optional[str] = None) -> dict:
|
|
|
121
126
|
info["torch"] = torch.__version__
|
|
122
127
|
info["cuda"] = torch.version.cuda
|
|
123
128
|
info["cuda_available"] = bool(torch.cuda.is_available())
|
|
124
|
-
except Exception:
|
|
129
|
+
except Exception as exc:
|
|
125
130
|
info["torch"] = None
|
|
131
|
+
info["torch_error"] = type(exc).__name__ + ": " + str(exc)
|
|
126
132
|
info["cuda"] = None
|
|
127
133
|
info["cuda_available"] = False
|
|
128
134
|
exec(NPU_PROBE, {"out": info})
|
|
@@ -130,6 +130,8 @@ class StateStore:
|
|
|
130
130
|
self._conn.execute("ALTER TABLE nodes ADD COLUMN gpu_devices TEXT")
|
|
131
131
|
if "accelerator_backend" not in cols:
|
|
132
132
|
self._conn.execute("ALTER TABLE nodes ADD COLUMN accelerator_backend TEXT")
|
|
133
|
+
if "accelerator_probe_error" not in cols:
|
|
134
|
+
self._conn.execute("ALTER TABLE nodes ADD COLUMN accelerator_probe_error TEXT")
|
|
133
135
|
run_cols = {r[1] for r in self._conn.execute("PRAGMA table_info(runs)").fetchall()}
|
|
134
136
|
for column, definition in (("queue_order", "INTEGER"), ("tune_yield", "INTEGER")):
|
|
135
137
|
if column not in run_cols:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Schema 说明
|
|
2
2
|
|
|
3
|
+
0.2.1:节点新增可空 `accelerator_probe_error`,SQLite 使用增量迁移。Tune counts/trials 新增 CANCELLED 展示状态;父任务终止且实际进程退出后,从旧快照投影终态,不改写 Ray checkpoint。CPU-only runtime 的 `accelerator_backend` 为 cpu。
|
|
4
|
+
|
|
3
5
|
Handfree REXP 插件通过 SSH 调用被控端 `rexp platform info --json`。返回值中的
|
|
4
6
|
`platform_root` 必须与该连接配置的绝对 `platformRoot` 一致;否则插件拒绝上传。
|
|
5
7
|
`connectionName` 只由 Handfree 的 SSH 主机配置解析,不是平台事实 schema 的字段。
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# REXP 使用说明书(v0.1)
|
|
2
2
|
|
|
3
|
+
## 0.2.1 部署修复
|
|
4
|
+
|
|
5
|
+
独立环境安装后执行同根目录的 `rexp init`,自动登记全局命令:Linux root 使用 `/usr/local/bin/rexp`,普通用户使用 `~/.local/bin/rexp`;Windows 创建用户级 `rexp.cmd` 并登记 PATH。不会覆盖无关同名程序;需要加载新 PATH 时打开新终端。验收必须直接输入 `rexp version` 和 `rexp doctor --json`,无需激活实验环境。`.platform/bootstrap` 是 uv 引导环境,不是实验环境。
|
|
6
|
+
|
|
7
|
+
非登录 SSH 的 Ascend 探测、实验解释器探测与 runner 自动补齐已存在的标准库路径,保留用户配置和可见设备。优先采用显式 `ASCEND_HOME_PATH` / `ASCEND_TOOLKIT_HOME`,再识别 CANN 9 的 `/usr/local/Ascend/cann` 或旧版 `ascend-toolkit/latest`,每次只选一套工具包。探测失败返回 `accelerator_probe_error`,不再无声显示为没有设备;Torch 导入失败保留原始原因,不重复导入产生误导错误。自定义驱动安装仍需自行配置兼容路径。
|
|
8
|
+
|
|
9
|
+
取消任务后以 `management.process_alive=false` 确认退出,再更新结束时间与 Tune 的取消状态。停止调度器会处理已经完成但尚未自动评测的记录。日志按节点时区显示并标明时区名称;JSON 时间仍为 UTC。CPU-only Tune runtime 标为 cpu。
|
|
10
|
+
|
|
11
|
+
SSH-MCP 安装清单可通过 `dependencyIndex` 选择依赖镜像,REXP wheel 本体始终取自正式 PyPI 并校验。安装支持 MCP progress;超时先检查目标实际状态,再显式重试。清单与已安装 skill 应同步更新。
|
|
12
|
+
|
|
3
13
|
Research Experiment Platform:单节点、离线优先的个人科研实验平台。
|
|
4
14
|
目标:Idea → Research Project → Experiment → Run → Artifact → Evaluation → Analysis → Conclusion,
|
|
5
15
|
全程可恢复、可复现、可追溯,且不用手动管理一堆脚本和文件。
|
|
@@ -41,6 +51,10 @@ Windows 安装后的命令通常是 `<Conda环境>\Scripts\rexp.exe`。
|
|
|
41
51
|
其他实验环境用 `rexp node env create NAME --python 3.11 --description "中文用途及限制"` 创建、登记在 `<根目录>/env/NAME`;uv/Conda 可按版本创建,venv 需要已安装的 Python 可执行文件(可用 `--python /path/to/python`)。优先复用已有兼容环境;云镜像的 TorchNPU/CANN 环境也可用 `node env add --python` 登记,不重装驱动。Conda prefix 环境与 uv/venv 一样由平台记录绝对解释器路径;既有 Conda 命名环境继续支持 `--conda-env` 登记。`env/platform` 保留给平台,不能登记为普通实验环境。
|
|
42
52
|
|
|
43
53
|
环境设置失败保留数据根目录,修复原因后重跑相同 init。依赖下载失败但解释器已建好时可重试安装;解释器创建中断留下不完整目录时会提示先检查,不自动删除。首次初始化保存用户默认根目录;已有默认根目录时用 `--save-default` 显式切换。安装后分别检查插件内和不设置 PLATFORM_ROOT 的普通终端 `rexp doctor --json`,确认根目录、绑定管理器、平台解释器及 default 环境一致。
|
|
54
|
+
|
|
55
|
+
Agent 首次询问管理器时应给出完整三项:安装 uv(推荐)、安装 Conda、使用现有 Python venv(不安装管理器)。推荐 uv 不会移除 Conda 选项。环境创建/安装显示当前步骤;pip/uv 默认每次网络请求超时 15 秒、重试一次,用户显式配置优先,整个初始化仍有独立总时限。
|
|
56
|
+
|
|
57
|
+
离线安装先准备匹配远端 OS/架构/Python ABI 的完整 wheelhouse,然后设置 `UV_OFFLINE=true UV_FIND_LINKS=<目录> PIP_NO_INDEX=1 PIP_FIND_LINKS=<目录>` 执行相同 init。`uv venv --seed` 也需要种子包(所选 Python 所需的 pip/setuptools/wheel);集合应另包含 default 的 NumPy 等依赖。仅设置 `UV_NO_INDEX=1` 不能让 uv 离线。已有 wheelhouse 时直接离线使用;失败后检查环境,不重复长时间联网尝试。
|
|
44
58
|
`rexp doctor --json` 与 `rexp platform info --json` 的 `experiment_setup` 返回登记状态:
|
|
45
59
|
`needs_environment` 表示没有实验环境,`registered` 只代表已登记,实际实验仍须检查项目依赖。
|
|
46
60
|
至少登记一个可用实验环境;未登记名为 `default` 的环境时,在实验 YAML 中显式指定 `environment:`,
|
|
@@ -41,7 +41,21 @@ def select_manager(available, requested="auto", existing=None):
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def run(command, timeout=900):
|
|
44
|
-
|
|
44
|
+
# A failed index request should not consume pip/uv's long default retries.
|
|
45
|
+
# Explicit operator settings (including offline wheelhouses) still win.
|
|
46
|
+
child_env = os.environ.copy()
|
|
47
|
+
for key, value in {"PIP_DEFAULT_TIMEOUT": "15", "PIP_RETRIES": "1",
|
|
48
|
+
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
|
|
49
|
+
"UV_HTTP_TIMEOUT": "15", "UV_HTTP_RETRIES": "1"}.items():
|
|
50
|
+
child_env.setdefault(key, value)
|
|
51
|
+
argv = [str(x) for x in command]
|
|
52
|
+
print(f"Environment step: {Path(argv[0]).name} {' '.join(argv[1:3])}", flush=True)
|
|
53
|
+
try:
|
|
54
|
+
result = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, env=child_env)
|
|
55
|
+
except subprocess.TimeoutExpired as exc:
|
|
56
|
+
raise RuntimeError(f"Environment step timed out after {timeout}s. Inspect the existing environment before retrying; "
|
|
57
|
+
"for a prepared wheelhouse use UV_OFFLINE=true UV_FIND_LINKS=<directory> "
|
|
58
|
+
"and PIP_NO_INDEX=1 PIP_FIND_LINKS=<directory>.") from exc
|
|
45
59
|
if result.returncode:
|
|
46
60
|
raise RuntimeError(f"Environment command failed: {(result.stderr or result.stdout)[-1500:]}")
|
|
47
61
|
return result.stdout
|
|
@@ -178,3 +192,57 @@ def configured_runtime(args):
|
|
|
178
192
|
return None
|
|
179
193
|
os.environ["REXP_RUNTIME_FORWARDED"] = "1"
|
|
180
194
|
return target
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def register_command(runtime_python, bin_directory=None, *, update_path=True):
|
|
198
|
+
"""Expose the configured runtime outside its virtual environment.
|
|
199
|
+
|
|
200
|
+
Never replace an unrelated command. User installs use ~/.local/bin; root
|
|
201
|
+
installs use /usr/local/bin. A Windows .cmd launcher uses the user PATH.
|
|
202
|
+
"""
|
|
203
|
+
import shlex
|
|
204
|
+
if bin_directory is None:
|
|
205
|
+
if os.name == 'nt':
|
|
206
|
+
bin_directory = Path(os.environ.get('LOCALAPPDATA', Path.home() / 'AppData/Local')) / 'rexprun/bin'
|
|
207
|
+
else:
|
|
208
|
+
bin_directory = Path('/usr/local/bin') if hasattr(os, 'geteuid') and os.geteuid() == 0 else Path.home() / '.local/bin'
|
|
209
|
+
directory = Path(bin_directory)
|
|
210
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
command = directory / ('rexp.cmd' if os.name == 'nt' else 'rexp')
|
|
212
|
+
marker = 'Managed by rexprun'
|
|
213
|
+
text = (f'@echo off\nrem {marker}\n"{runtime_python}" -m rexp %*\n' if os.name == 'nt' else
|
|
214
|
+
f'#!/bin/sh\n# {marker}\nexec {shlex.quote(str(runtime_python))} -m rexp "$@"\n')
|
|
215
|
+
if command.exists() or command.is_symlink():
|
|
216
|
+
try:
|
|
217
|
+
existing = command.read_text(encoding='utf-8')
|
|
218
|
+
except (OSError, UnicodeError):
|
|
219
|
+
existing = ''
|
|
220
|
+
if marker not in existing:
|
|
221
|
+
# An already globally installed console entry is valid, too.
|
|
222
|
+
if 'rexp.cli.main' in existing:
|
|
223
|
+
return {'path': str(command), 'created': False}
|
|
224
|
+
raise ValueError(f'Command {command} already exists and is not managed by rexprun; it was not overwritten.')
|
|
225
|
+
command.write_text(text, encoding='utf-8')
|
|
226
|
+
if os.name != 'nt':
|
|
227
|
+
command.chmod(0o755)
|
|
228
|
+
in_path = str(directory) in os.environ.get('PATH', '').split(os.pathsep)
|
|
229
|
+
if update_path and not in_path:
|
|
230
|
+
if os.name == 'nt':
|
|
231
|
+
import winreg
|
|
232
|
+
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, 'Environment') as key:
|
|
233
|
+
try:
|
|
234
|
+
value, _ = winreg.QueryValueEx(key, 'Path')
|
|
235
|
+
except FileNotFoundError:
|
|
236
|
+
value = ''
|
|
237
|
+
if str(directory).lower() not in value.lower().split(';'):
|
|
238
|
+
winreg.SetValueEx(key, 'Path', 0, winreg.REG_EXPAND_SZ, value.rstrip(';') + ';' + str(directory))
|
|
239
|
+
else:
|
|
240
|
+
line = f'\n# {marker}\nexport PATH={shlex.quote(str(directory))}:"$PATH"\n'
|
|
241
|
+
profiles = ['.zprofile', '.zshrc'] if 'zsh' in os.environ.get('SHELL', '') else ['.profile', '.bashrc']
|
|
242
|
+
for name in profiles:
|
|
243
|
+
profile = Path.home() / name
|
|
244
|
+
current = profile.read_text() if profile.exists() else ''
|
|
245
|
+
if line.strip() not in current:
|
|
246
|
+
with profile.open('a') as output:
|
|
247
|
+
output.write(line)
|
|
248
|
+
return {'path': str(command), 'created': True, 'new_terminal_required': not in_path}
|
|
@@ -12,12 +12,12 @@ import re
|
|
|
12
12
|
import socket
|
|
13
13
|
import subprocess
|
|
14
14
|
|
|
15
|
-
from rexp.core.accelerators import ascend_visible_ids
|
|
15
|
+
from rexp.core.accelerators import ascend_visible_ids, ascend_runtime_environment
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
HARDWARE_FIELDS = (
|
|
19
19
|
"hostname", "os", "cpu_cores", "ram_gb", "gpu_name", "gpu_count",
|
|
20
|
-
"gpu_vram_mb", "gpu_driver", "gpu_devices", "accelerator_backend",
|
|
20
|
+
"gpu_vram_mb", "gpu_driver", "gpu_devices", "accelerator_backend", "accelerator_probe_error",
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
|
|
@@ -33,7 +33,8 @@ def detect_local_node(platform_version: str = "") -> dict:
|
|
|
33
33
|
hostname = socket.gethostname()
|
|
34
34
|
cpu_cores = _available_cpu_cores()
|
|
35
35
|
ram_gb = _available_ram_gb()
|
|
36
|
-
|
|
36
|
+
diagnostics = {}
|
|
37
|
+
gpu_name, gpu_count, gpu_vram_mb, driver, devices = _detect_gpus(diagnostics)
|
|
37
38
|
return {
|
|
38
39
|
"hostname": hostname,
|
|
39
40
|
"os": _platform.platform(),
|
|
@@ -45,6 +46,7 @@ def detect_local_node(platform_version: str = "") -> dict:
|
|
|
45
46
|
"gpu_driver": driver,
|
|
46
47
|
"gpu_devices": json.dumps(devices),
|
|
47
48
|
"accelerator_backend": devices[0].get("backend", "cuda") if devices else None,
|
|
49
|
+
"accelerator_probe_error": diagnostics.get("error"),
|
|
48
50
|
# Keep legacy DB columns cleared; CUDA/PyTorch belong to experiment envs.
|
|
49
51
|
"cuda": None,
|
|
50
52
|
"torch": None,
|
|
@@ -156,9 +158,9 @@ def _total_ram_gb() -> float:
|
|
|
156
158
|
return 0.0
|
|
157
159
|
|
|
158
160
|
|
|
159
|
-
def _detect_gpus() -> tuple[str, int, int, str, list[dict]]:
|
|
161
|
+
def _detect_gpus(diagnostics=None) -> tuple[str, int, int, str, list[dict]]:
|
|
160
162
|
detected = _detect_nvidia_gpus()
|
|
161
|
-
return detected if detected[1] else _detect_npus()
|
|
163
|
+
return detected if detected[1] else _detect_npus(diagnostics)
|
|
162
164
|
|
|
163
165
|
|
|
164
166
|
def _detect_nvidia_gpus() -> tuple[str, int, int, str, list[dict]]:
|
|
@@ -191,16 +193,19 @@ def _detect_nvidia_gpus() -> tuple[str, int, int, str, list[dict]]:
|
|
|
191
193
|
return "", 0, 0, "", []
|
|
192
194
|
|
|
193
195
|
|
|
194
|
-
def _detect_npus() -> tuple[str, int, int, str, list[dict]]:
|
|
196
|
+
def _detect_npus(diagnostics=None) -> tuple[str, int, int, str, list[dict]]:
|
|
195
197
|
"""Read Ascend board/chip/runtime logical mappings without importing torch.
|
|
196
198
|
|
|
197
199
|
First delivery targets homogeneous, unpartitioned Ascend Linux nodes.
|
|
198
200
|
Unknown topology is not guessed: no fabricated contiguous device IDs.
|
|
199
201
|
"""
|
|
202
|
+
diagnostics = diagnostics if diagnostics is not None else {}
|
|
203
|
+
runtime_env = ascend_runtime_environment()
|
|
200
204
|
try:
|
|
201
205
|
result = subprocess.run(["npu-smi", "info", "-m"], capture_output=True,
|
|
202
|
-
text=True, timeout=15)
|
|
206
|
+
text=True, timeout=15, env=runtime_env)
|
|
203
207
|
if result.returncode != 0:
|
|
208
|
+
diagnostics["error"] = "npu-smi failed: " + (getattr(result, "stderr", "") or result.stdout)[-1000:]
|
|
204
209
|
return "", 0, 0, "", []
|
|
205
210
|
devices = []
|
|
206
211
|
seen = set()
|
|
@@ -221,7 +226,7 @@ def _detect_npus() -> tuple[str, int, int, str, list[dict]]:
|
|
|
221
226
|
try:
|
|
222
227
|
memory = subprocess.run(
|
|
223
228
|
["npu-smi", "info", "-t", "memory", "-i", str(card), "-c", str(chip)],
|
|
224
|
-
capture_output=True, text=True, timeout=10)
|
|
229
|
+
capture_output=True, text=True, timeout=10, env=runtime_env)
|
|
225
230
|
if memory.returncode == 0:
|
|
226
231
|
capacity = re.search(r"(?:HBM|Memory)\s+Capacity\s*\(\s*MB\s*\)\s*:\s*(\d+)",
|
|
227
232
|
memory.stdout, re.IGNORECASE)
|
|
@@ -232,8 +237,13 @@ def _detect_npus() -> tuple[str, int, int, str, list[dict]]:
|
|
|
232
237
|
devices.append({"index": logical, "uuid": None, "name": " ".join(fields[name_start:]),
|
|
233
238
|
"memory_mb": memory_mb, "backend": "npu",
|
|
234
239
|
"card_id": card, "chip_id": chip})
|
|
240
|
+
if not devices and allowed is None:
|
|
241
|
+
diagnostics["error"] = "npu-smi returned no recognized logical devices; inspect npu-smi info -m"
|
|
235
242
|
devices.sort(key=lambda device: device["index"])
|
|
236
243
|
return (", ".join(d["name"] for d in devices), len(devices),
|
|
237
244
|
max((d["memory_mb"] for d in devices), default=0), "", devices)
|
|
238
|
-
except
|
|
245
|
+
except FileNotFoundError:
|
|
246
|
+
return "", 0, 0, "", []
|
|
247
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
248
|
+
diagnostics["error"] = f"npu-smi probe failed: {exc}"
|
|
239
249
|
return "", 0, 0, "", []
|
|
@@ -10,6 +10,8 @@ import argparse
|
|
|
10
10
|
import importlib.util
|
|
11
11
|
import json
|
|
12
12
|
import logging
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from zoneinfo import ZoneInfo
|
|
13
15
|
import sys
|
|
14
16
|
import threading
|
|
15
17
|
import traceback
|
|
@@ -46,7 +48,12 @@ class ExperimentContext:
|
|
|
46
48
|
logger = logging.getLogger(f"rexp.run.{self.run_id}")
|
|
47
49
|
logger.setLevel(logging.INFO)
|
|
48
50
|
if not logger.handlers:
|
|
49
|
-
|
|
51
|
+
from rexp.core.config import load_nodes_config
|
|
52
|
+
tz = ZoneInfo(load_nodes_config(self.paths).schedule.timezone)
|
|
53
|
+
class LocalFormatter(logging.Formatter):
|
|
54
|
+
def formatTime(self, record, datefmt=None):
|
|
55
|
+
return datetime.fromtimestamp(record.created, tz).strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
56
|
+
fmt = LocalFormatter("%(asctime)s %(levelname)s %(message)s")
|
|
50
57
|
fh = logging.FileHandler(self.run_dir / "logs" / "experiment.log", encoding="utf-8")
|
|
51
58
|
fh.setFormatter(fmt)
|
|
52
59
|
logger.addHandler(fh)
|
|
@@ -146,6 +153,7 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|
|
146
153
|
experiment.setup(ctx)
|
|
147
154
|
experiment.run(ctx)
|
|
148
155
|
if (store.get_run(args.run_id) or {}).get("status") == ExperimentStatus.CANCELLED.value:
|
|
156
|
+
store.update_run(args.run_id, finished_at=now_iso(), exit_code=0)
|
|
149
157
|
ctx.logger.info("Run cancelled before finalization")
|
|
150
158
|
return 0
|
|
151
159
|
if hasattr(experiment, "finalize"):
|
|
@@ -173,6 +181,7 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|
|
173
181
|
except Exception as exc: # noqa: BLE001
|
|
174
182
|
traceback.print_exc(file=sys.stderr)
|
|
175
183
|
if (store.get_run(args.run_id) or {}).get("status") == ExperimentStatus.CANCELLED.value:
|
|
184
|
+
store.update_run(args.run_id, finished_at=now_iso(), exit_code=0)
|
|
176
185
|
ctx.logger.info("Run cancelled during shutdown: %s", exc)
|
|
177
186
|
return 0
|
|
178
187
|
store.update_run(
|
|
@@ -213,6 +213,8 @@ class Scheduler:
|
|
|
213
213
|
except Exception:
|
|
214
214
|
pass
|
|
215
215
|
self._procs.clear()
|
|
216
|
+
self._release_finished_allocations()
|
|
217
|
+
self._maybe_evaluate_all(load_platform_config(self.paths))
|
|
216
218
|
if (self.store.get_platform_status() or {}).get("scheduler_pid") == os.getpid():
|
|
217
219
|
self.store.set_platform_status(PlatformStatus.OFF.value, stopped_at=now_iso())
|
|
218
220
|
|
|
@@ -385,6 +387,8 @@ class Scheduler:
|
|
|
385
387
|
if not alive:
|
|
386
388
|
self.store.update_trial_lease(lease['lease_id'], status='RELEASED')
|
|
387
389
|
for run in self.store.list_runs():
|
|
390
|
+
if run["status"] == "CANCELLED" and not is_process_group_alive(run.get("pid")) and not run.get("finished_at"):
|
|
391
|
+
self.store.update_run(run["run_id"], finished_at=now_iso())
|
|
388
392
|
if run.get("allocated_gpu_ids") is not None and run.get("pid"):
|
|
389
393
|
if not is_process_group_alive(run["pid"]):
|
|
390
394
|
self.store.update_run(run["run_id"], pid=None)
|
|
@@ -542,7 +546,8 @@ class Scheduler:
|
|
|
542
546
|
"--run-id", run["run_id"]]
|
|
543
547
|
if run.get("resume_requested"):
|
|
544
548
|
cmd.append("--resume")
|
|
545
|
-
|
|
549
|
+
from rexp.core.accelerators import ascend_runtime_environment
|
|
550
|
+
env = ascend_runtime_environment()
|
|
546
551
|
env["PYTHONPATH"] = str(import_root) + os.pathsep + env.get("PYTHONPATH", "")
|
|
547
552
|
env["REXP_PLATFORM_ROOT"] = str(self.paths.root)
|
|
548
553
|
env = (search_controller_environment(node_info, env) if exp_config.tune
|
|
@@ -262,7 +262,11 @@ def cancel_run(paths: Paths, run_id: str) -> dict:
|
|
|
262
262
|
pause_file.write_text(now_iso(), encoding="utf-8")
|
|
263
263
|
result = store.get_run(run_id)
|
|
264
264
|
from rexp.core.process import is_process_group_alive
|
|
265
|
-
|
|
265
|
+
alive = is_process_group_alive(run.get("pid"))
|
|
266
|
+
if not alive:
|
|
267
|
+
store.update_run(run_id, finished_at=now_iso())
|
|
268
|
+
result = store.get_run(run_id)
|
|
269
|
+
result["cancellation"] = {"requested": True, "process_alive": alive}
|
|
266
270
|
return result
|
|
267
271
|
finally:
|
|
268
272
|
store.close()
|
|
@@ -93,6 +93,11 @@ def search_details(paths, run_id, *, include_trials=True):
|
|
|
93
93
|
enabled = bool(run['tune_yield']) if run.get('tune_yield') is not None else data['search'].get('yield_to_normal', False)
|
|
94
94
|
data['search']['yield_to_normal'] = enabled
|
|
95
95
|
data['trial_priority'] = int(enabled)
|
|
96
|
+
from rexp.core.process import is_process_group_alive, is_process_alive
|
|
97
|
+
parent_exited = not is_process_group_alive(run.get("pid")) and not any(is_process_alive(l["pid"]) or is_process_group_alive(l["pid"]) for l in leases)
|
|
98
|
+
parent_terminal = run["status"] in {"COMPLETED", "FAILED", "CANCELLED"}
|
|
99
|
+
inactive_status = "CANCELLED" if run["status"] == "CANCELLED" else "ERROR" if run["status"] in {"COMPLETED", "FAILED"} else "PAUSED"
|
|
100
|
+
stopped = parent_exited and (parent_terminal or run["status"] in {"PAUSED", "RECOVERABLE"})
|
|
96
101
|
active_devices = set()
|
|
97
102
|
if include_trials:
|
|
98
103
|
planned_samples = ExperimentConfig.from_yaml_text(run['config_snapshot']).total_samples
|
|
@@ -104,17 +109,25 @@ def search_details(paths, run_id, *, include_trials=True):
|
|
|
104
109
|
if lease and row['status'] not in {'TERMINATED', 'ERROR', 'PAUSED'}:
|
|
105
110
|
row['status'] = 'PENDING' if lease['status'] == 'REQUESTED' else 'RUNNING'
|
|
106
111
|
row['wait_reason'] = lease['wait_reason']
|
|
107
|
-
if
|
|
112
|
+
if stopped and row["status"] not in {"TERMINATED", "ERROR", "CANCELLED"} and not (lease and is_process_alive(lease["pid"])):
|
|
113
|
+
row["status"] = inactive_status
|
|
114
|
+
row["finished_at"] = run.get("finished_at") or row.get("finished_at")
|
|
115
|
+
row["wait_reason"] = run.get("wait_reason")
|
|
116
|
+
if lease and lease['status'] == 'GRANTED' and not stopped:
|
|
108
117
|
row['allocated_gpu_ids'] = json.loads(lease['allocated_gpu_ids'] or '[]')
|
|
109
118
|
data['trials'] = sorted_trials(data['trials'], data['search']['metrics'], data['pareto_trial_ids'])
|
|
110
119
|
data['counts'] = {status: sum(r['status'] == status for r in data['trials'])
|
|
111
|
-
for status in ('PENDING', 'RUNNING', 'PAUSED', 'TERMINATED', 'ERROR')}
|
|
120
|
+
for status in ('PENDING', 'RUNNING', 'PAUSED', 'TERMINATED', 'ERROR', 'CANCELLED')}
|
|
112
121
|
else:
|
|
113
|
-
|
|
122
|
+
if stopped:
|
|
123
|
+
remaining = sum(data["counts"].pop(k, 0) for k in ("RUNNING", "PENDING"))
|
|
124
|
+
data["counts"]["RUNNING"] = data["counts"]["PENDING"] = 0
|
|
125
|
+
data["counts"][inactive_status] = data["counts"].get(inactive_status, 0) + remaining
|
|
126
|
+
waiting = 0 if stopped else sum(l['status'] == 'REQUESTED' for l in leases)
|
|
114
127
|
data['counts']['RUNNING'] = max(0, data['counts'].get('RUNNING', 0) - waiting)
|
|
115
128
|
data['counts']['PENDING'] = data['counts'].get('PENDING', 0) + waiting
|
|
116
129
|
for lease in leases:
|
|
117
|
-
if lease['status'] in {'GRANTED', 'RELEASING'}:
|
|
130
|
+
if lease['status'] in {'GRANTED', 'RELEASING'} and not stopped:
|
|
118
131
|
active_devices.update(json.loads(lease['allocated_gpu_ids'] or '[]'))
|
|
119
132
|
return {**data, "status": run["status"], "project": run["project"], "version": run["version"],
|
|
120
133
|
"allocated_gpu_ids": sorted(active_devices),
|
|
@@ -248,13 +261,13 @@ def _trial_entry(parameters, *, entry_file, run_dir, experiment, root, run_id, c
|
|
|
248
261
|
artifacts.mkdir(parents=True, exist_ok=True)
|
|
249
262
|
backend = os.environ.get('REXP_ACCELERATOR_BACKEND', 'cuda')
|
|
250
263
|
visible = os.environ.get('ASCEND_RT_VISIBLE_DEVICES' if backend == 'npu' else 'CUDA_VISIBLE_DEVICES', '')
|
|
251
|
-
write_json(artifacts / 'runtime.json', {'accelerator_backend': backend, 'visible_devices': visible, 'allocated_gpu_ids': devices, 'pid': os.getpid(), 'execution_started_at': now_iso()})
|
|
264
|
+
write_json(artifacts / 'runtime.json', {'accelerator_backend': backend if devices else 'cpu', 'visible_devices': visible, 'allocated_gpu_ids': devices, 'pid': os.getpid(), 'execution_started_at': now_iso()})
|
|
252
265
|
spec = importlib.util.spec_from_file_location('rexp_tune_trainable', entry_file)
|
|
253
266
|
module = importlib.util.module_from_spec(spec)
|
|
254
267
|
sys.path.insert(0, str(Path(entry_file).parent))
|
|
255
268
|
sys.path.insert(0, str(Path(entry_file).parent.parent))
|
|
256
269
|
spec.loader.exec_module(module)
|
|
257
|
-
config = {**parameters, '_rexp': {'artifact_dir': str(artifacts), 'trial_id': trial_id, 'experiment': experiment, 'accelerator_backend': backend, 'allocated_device_ids': devices}}
|
|
270
|
+
config = {**parameters, '_rexp': {'artifact_dir': str(artifacts), 'trial_id': trial_id, 'experiment': experiment, 'accelerator_backend': backend if devices else 'cpu', 'allocated_device_ids': devices}}
|
|
258
271
|
result = module.trainable(config)
|
|
259
272
|
if result is not None:
|
|
260
273
|
tune.report(result)
|
|
@@ -378,7 +391,7 @@ class TuneExperiment:
|
|
|
378
391
|
sampler=optuna.samplers.TPESampler(seed=self.config.seed))
|
|
379
392
|
else:
|
|
380
393
|
from ray.tune.search.basic_variant import BasicVariantGenerator
|
|
381
|
-
searcher = BasicVariantGenerator(random_state=self.config.seed)
|
|
394
|
+
searcher = BasicVariantGenerator(random_state=self.config.seed, max_concurrent=cfg.max_concurrent_trials)
|
|
382
395
|
scheduler = None
|
|
383
396
|
if cfg.scheduler == "asha":
|
|
384
397
|
from ray.tune.schedulers import ASHAScheduler
|
|
@@ -387,7 +400,7 @@ class TuneExperiment:
|
|
|
387
400
|
tuner = tune.Tuner(trainable, param_space=space,
|
|
388
401
|
tune_config=tune.TuneConfig(metric=keys[0] if len(keys) == 1 else None,
|
|
389
402
|
mode=modes[0] if len(keys) == 1 else None, search_alg=searcher, scheduler=scheduler,
|
|
390
|
-
num_samples=cfg.num_samples, max_concurrent_trials=cfg.max_concurrent_trials, reuse_actors=False,
|
|
403
|
+
num_samples=cfg.num_samples, max_concurrent_trials=cfg.max_concurrent_trials if cfg.search_algorithm == "optuna" else None, reuse_actors=False,
|
|
391
404
|
time_budget_s=cfg.time_budget_seconds),
|
|
392
405
|
run_config=tune.RunConfig(name="search", storage_path=str(storage), callbacks=[monitor], verbose=0,
|
|
393
406
|
failure_config=tune.FailureConfig(max_failures=cfg.max_failures),
|
|
@@ -5,6 +5,8 @@ description: 为 REXP 设计实验、样本进度、适度频率的断点与评
|
|
|
5
5
|
|
|
6
6
|
# Experiment Authoring Skill
|
|
7
7
|
|
|
8
|
+
0.2.1 补充:在实验解释器内验证 TorchNPU,不给平台解释器安装科研框架。`doctor` 的 `accelerator_probe_error` 表示探测失败,不能当成没有设备。自定义 CANN 安装仍须准备兼容环境。Ray CPU 模板只在结束时保存计算 checkpoint;真实实验按代价选择保存间隔。真卡数值短测与持续负载展示用途不同,瞬时 0% 不能证明未使用 NPU。模板中的 `<PLATFORM_ROOT>` 必须替换成查询得到的绝对根目录,不依赖 SSH 当前目录。
|
|
9
|
+
|
|
8
10
|
本 Skill 供科研 AI Agent 使用:把用户的科研 Idea 变成规范的 Research Project、
|
|
9
11
|
Experiment、Evaluation、Analysis 与 Config,并确保平台规定的目录与资源纪律。
|
|
10
12
|
|
|
@@ -150,7 +152,7 @@ PAUSED/RECOVERABLE 等可续跑任务若确需放弃,应先明确取消,不
|
|
|
150
152
|
以下每行是独立命令示例,尖括号中的值由实际项目替换:
|
|
151
153
|
|
|
152
154
|
```text
|
|
153
|
-
rexp project create <project> --version v1
|
|
155
|
+
rexp project create <project> --version v1
|
|
154
156
|
rexp submit <project> v1 --config <PLATFORM_ROOT>/research/<project>/v1/configs/smoke.yaml
|
|
155
157
|
rexp start
|
|
156
158
|
rexp status
|