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.
Files changed (119) hide show
  1. {rexprun-0.2.0 → rexprun-0.2.2}/MANIFEST.in +1 -0
  2. {rexprun-0.2.0/rexprun.egg-info → rexprun-0.2.2}/PKG-INFO +1 -1
  3. {rexprun-0.2.0 → rexprun-0.2.2}/pyproject.toml +1 -1
  4. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/main.py +7 -1
  5. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/README.md +1 -1
  6. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/README.md +1 -1
  7. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune/README.md +1 -1
  8. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune/experiment/trainable.py +5 -2
  9. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune_npu/README.md +1 -1
  10. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/accelerators.py +41 -0
  11. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/provenance.py +8 -2
  12. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/state_store.py +2 -0
  13. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/docs/schemas.md +2 -0
  14. {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
  15. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/environments.py +69 -1
  16. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/node.py +19 -9
  17. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/runner.py +10 -1
  18. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/scheduler.py +6 -1
  19. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/services.py +5 -1
  20. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/tuning.py +21 -8
  21. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/skills/experiment-authoring/SKILL.md +3 -1
  22. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/skills/platform-operations/SKILL.md +16 -0
  23. rexprun-0.2.2/rexp/version.py +1 -0
  24. {rexprun-0.2.0 → rexprun-0.2.2/rexprun.egg-info}/PKG-INFO +1 -1
  25. {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/SOURCES.txt +1 -0
  26. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_environment_setup.py +21 -0
  27. rexprun-0.2.2/tests/test_first_use_regressions.py +117 -0
  28. rexprun-0.2.0/rexp/version.py +0 -1
  29. {rexprun-0.2.0 → rexprun-0.2.2}/LICENSE +0 -0
  30. {rexprun-0.2.0 → rexprun-0.2.2}/README.md +0 -0
  31. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/__init__.py +0 -0
  32. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/__main__.py +0 -0
  33. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/__init__.py +0 -0
  34. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ascend_smoke/README.md +0 -0
  35. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ascend_smoke/configs/smoke.yaml +0 -0
  36. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ascend_smoke/experiment/experiment.py +0 -0
  37. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/analysis/analyze.py +0 -0
  38. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/configs/full.yaml +0 -0
  39. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/configs/smoke.yaml +0 -0
  40. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/evaluation/summarize.py +0 -0
  41. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/experiment/README.md +0 -0
  42. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/experiment/experiment.py +0 -0
  43. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/experiment/experiment_helpers.py +0 -0
  44. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/default/tests/test_experiment.py +0 -0
  45. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/configs/full.yaml +0 -0
  46. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/configs/smoke.yaml +0 -0
  47. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/evaluation/kv_error.py +0 -0
  48. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/dlm_kv_staleness/experiment/kv_staleness.py +0 -0
  49. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune/configs/smoke.yaml +0 -0
  50. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune_npu/configs/smoke.yaml +0 -0
  51. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/cli/templates/ray_tune_npu/experiment/trainable.py +0 -0
  52. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/__init__.py +0 -0
  53. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/conda.py +0 -0
  54. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/config.py +0 -0
  55. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/machine.py +0 -0
  56. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/paths.py +0 -0
  57. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/process.py +0 -0
  58. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/resources.py +0 -0
  59. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/run_lock.py +0 -0
  60. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/schemas.py +0 -0
  61. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/states.py +0 -0
  62. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/storage.py +0 -0
  63. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/core/tuning.py +0 -0
  64. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/datasets/__init__.py +0 -0
  65. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/datasets/base.py +0 -0
  66. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/datasets/huggingface.py +0 -0
  67. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/datasets/local_jsonl.py +0 -0
  68. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/models/__init__.py +0 -0
  69. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/models/base.py +0 -0
  70. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/models/dream.py +0 -0
  71. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/models/llada.py +0 -0
  72. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/__init__.py +0 -0
  73. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/analysis.py +0 -0
  74. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/assets.py +0 -0
  75. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/dependencies.py +0 -0
  76. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/envprobe.py +0 -0
  77. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/evaluation.py +0 -0
  78. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/mcp.py +0 -0
  79. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/run_cleanup.py +0 -0
  80. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/script_execution.py +0 -0
  81. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/script_worker.py +0 -0
  82. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/setup.py +0 -0
  83. {rexprun-0.2.0 → rexprun-0.2.2}/rexp/server/trial_queue.py +0 -0
  84. {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/dependency_links.txt +0 -0
  85. {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/entry_points.txt +0 -0
  86. {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/requires.txt +0 -0
  87. {rexprun-0.2.0 → rexprun-0.2.2}/rexprun.egg-info/top_level.txt +0 -0
  88. {rexprun-0.2.0 → rexprun-0.2.2}/setup.cfg +0 -0
  89. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_adapters.py +0 -0
  90. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_ascend.py +0 -0
  91. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_assets.py +0 -0
  92. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_bundled_resources.py +0 -0
  93. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_cli.py +0 -0
  94. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_command_audit.py +0 -0
  95. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_crash_recovery.py +0 -0
  96. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_env_selection.py +0 -0
  97. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_environment_catalog.py +0 -0
  98. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_experiment_loading.py +0 -0
  99. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_initial_setup.py +0 -0
  100. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_lifecycle.py +0 -0
  101. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_network_config.py +0 -0
  102. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_pause_resume.py +0 -0
  103. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_platform_info.py +0 -0
  104. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_plugin_interfaces.py +0 -0
  105. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_process_linux.py +0 -0
  106. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_project_deps.py +0 -0
  107. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_research_catalog.py +0 -0
  108. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_retry.py +0 -0
  109. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_run_cleanup.py +0 -0
  110. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_run_metadata.py +0 -0
  111. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_runs_limit.py +0 -0
  112. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_scheduler_checks.py +0 -0
  113. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_scheduler_gpu.py +0 -0
  114. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_script_environment.py +0 -0
  115. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_skills_bundled.py +0 -0
  116. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_startup_resume.py +0 -0
  117. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_storage.py +0 -0
  118. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_trial_queue.py +0 -0
  119. {rexprun-0.2.0 → rexprun-0.2.2}/tests/test_tuning.py +0 -0
@@ -6,5 +6,6 @@ exclude .agents
6
6
 
7
7
  # Exclude caches and build leftovers from sdists
8
8
  global-exclude __pycache__ *.py[cod]
9
+ global-exclude .DS_Store
9
10
  recursive-exclude * __pycache__
10
11
  exclude HANDOFF.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rexprun
3
- Version: 0.2.0
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.0"
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
- tune.report({'score': score, 'samples_completed': step + 1},
24
- checkpoint=tune.Checkpoint.from_directory(str(state)))
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
- result = subprocess.run([str(x) for x in command], capture_output=True, text=True, timeout=timeout)
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
- gpu_name, gpu_count, gpu_vram_mb, driver, devices = _detect_gpus()
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 (OSError, subprocess.TimeoutExpired):
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
- fmt = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
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
- env = dict(os.environ)
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
- result["cancellation"] = {"requested": True, "process_alive": is_process_group_alive(run.get("pid"))}
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 lease and lease['status'] == 'GRANTED':
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
- waiting = sum(l['status'] == 'REQUESTED' for l in leases)
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 --domain dlm
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