starforge-core 0.1.6__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 (151) hide show
  1. starforge_core-0.1.6/.gitignore +36 -0
  2. starforge_core-0.1.6/PKG-INFO +10 -0
  3. starforge_core-0.1.6/gitlab-ci.example.yml +19 -0
  4. starforge_core-0.1.6/pyproject.toml +30 -0
  5. starforge_core-0.1.6/pytest.ini +3 -0
  6. starforge_core-0.1.6/scripts/build_and_check.sh +43 -0
  7. starforge_core-0.1.6/starforge_core/__init__.py +80 -0
  8. starforge_core-0.1.6/starforge_core/artifacts.py +140 -0
  9. starforge_core-0.1.6/starforge_core/benchmarks/__init__.py +20 -0
  10. starforge_core-0.1.6/starforge_core/benchmarks/runners.py +163 -0
  11. starforge_core-0.1.6/starforge_core/capsule.py +294 -0
  12. starforge_core-0.1.6/starforge_core/capsule_runner.py +100 -0
  13. starforge_core-0.1.6/starforge_core/config_resolve.py +391 -0
  14. starforge_core-0.1.6/starforge_core/contract/__init__.py +78 -0
  15. starforge_core-0.1.6/starforge_core/contract/binding.py +104 -0
  16. starforge_core-0.1.6/starforge_core/contract/env.py +193 -0
  17. starforge_core-0.1.6/starforge_core/contract/errors.py +18 -0
  18. starforge_core-0.1.6/starforge_core/contract/names.py +88 -0
  19. starforge_core-0.1.6/starforge_core/contract/spec.py +695 -0
  20. starforge_core-0.1.6/starforge_core/contract/topology.py +137 -0
  21. starforge_core-0.1.6/starforge_core/datasets.py +259 -0
  22. starforge_core-0.1.6/starforge_core/distributed/__init__.py +6 -0
  23. starforge_core-0.1.6/starforge_core/distributed/coordinator.py +148 -0
  24. starforge_core-0.1.6/starforge_core/distributed/protocol.py +52 -0
  25. starforge_core-0.1.6/starforge_core/distributed/ray_runtime.py +157 -0
  26. starforge_core-0.1.6/starforge_core/frameworks/__init__.py +24 -0
  27. starforge_core-0.1.6/starforge_core/frameworks/actions.py +61 -0
  28. starforge_core-0.1.6/starforge_core/frameworks/base.py +377 -0
  29. starforge_core-0.1.6/starforge_core/frameworks/custom.py +46 -0
  30. starforge_core-0.1.6/starforge_core/frameworks/evalkit.py +59 -0
  31. starforge_core-0.1.6/starforge_core/frameworks/nemo_rl.py +214 -0
  32. starforge_core-0.1.6/starforge_core/frameworks/observability.py +624 -0
  33. starforge_core-0.1.6/starforge_core/frameworks/registry.py +53 -0
  34. starforge_core-0.1.6/starforge_core/frameworks/runtime.py +164 -0
  35. starforge_core-0.1.6/starforge_core/frameworks/trl.py +161 -0
  36. starforge_core-0.1.6/starforge_core/frameworks/verl.py +274 -0
  37. starforge_core-0.1.6/starforge_core/launcher.py +410 -0
  38. starforge_core-0.1.6/starforge_core/lifecycle/__init__.py +2 -0
  39. starforge_core-0.1.6/starforge_core/lifecycle/benchmark_eval.py +107 -0
  40. starforge_core-0.1.6/starforge_core/lifecycle/export_complete.py +32 -0
  41. starforge_core-0.1.6/starforge_core/lifecycle/nemo_eval.py +65 -0
  42. starforge_core-0.1.6/starforge_core/lifecycle/nemo_export.py +105 -0
  43. starforge_core-0.1.6/starforge_core/lifecycle/verl_eval.py +197 -0
  44. starforge_core-0.1.6/starforge_core/metrics.py +75 -0
  45. starforge_core-0.1.6/starforge_core/package_bootstrap.py +73 -0
  46. starforge_core-0.1.6/starforge_core/pkce.py +39 -0
  47. starforge_core-0.1.6/starforge_core/plugins.py +311 -0
  48. starforge_core-0.1.6/starforge_core/recipes/__init__.py +188 -0
  49. starforge_core-0.1.6/starforge_core/recipes/catalog/custom/custom/recipe.yaml +45 -0
  50. starforge_core-0.1.6/starforge_core/recipes/catalog/custom/custom/template/README.md +9 -0
  51. starforge_core-0.1.6/starforge_core/recipes/catalog/custom/custom/template/train.sh +13 -0
  52. starforge_core-0.1.6/starforge_core/recipes/catalog/evalkit/benchmark/recipe.yaml +84 -0
  53. starforge_core-0.1.6/starforge_core/recipes/catalog/evalkit/benchmark/template/README.md +7 -0
  54. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/distillation/recipe.yaml +243 -0
  55. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/distillation/template/README.md +3 -0
  56. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/distillation/template/config.yaml +4 -0
  57. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/dpo/recipe.yaml +207 -0
  58. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/dpo/template/README.md +3 -0
  59. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/dpo/template/config.yaml +2 -0
  60. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo/recipe.yaml +467 -0
  61. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo/template/README.md +21 -0
  62. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo/template/config.yaml +4 -0
  63. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo-lora/recipe.yaml +185 -0
  64. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo-lora/template/README.md +5 -0
  65. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo-lora/template/config.yaml +10 -0
  66. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/maxrl/recipe.yaml +322 -0
  67. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/maxrl/template/README.md +3 -0
  68. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/maxrl/template/config.yaml +6 -0
  69. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/maxrl/template/run.py +4 -0
  70. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/opsd/recipe.yaml +237 -0
  71. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/opsd/template/README.md +3 -0
  72. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/opsd/template/config.yaml +6 -0
  73. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/opsd/template/run.py +4 -0
  74. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/ppo/recipe.yaml +287 -0
  75. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/ppo/template/README.md +4 -0
  76. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/ppo/template/config.yaml +5 -0
  77. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/rm/recipe.yaml +177 -0
  78. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/rm/template/README.md +4 -0
  79. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/rm/template/config.yaml +5 -0
  80. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/sft/recipe.yaml +202 -0
  81. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/sft/template/README.md +3 -0
  82. starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/sft/template/config.yaml +34 -0
  83. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/recipe.yaml +70 -0
  84. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/template/README.md +4 -0
  85. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/template/config.yaml +10 -0
  86. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/template/eval.py +12 -0
  87. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/template/train.py +41 -0
  88. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/recipe.yaml +80 -0
  89. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/template/README.md +4 -0
  90. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/template/config.yaml +12 -0
  91. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/template/eval.py +12 -0
  92. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/template/train.py +54 -0
  93. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/recipe.yaml +71 -0
  94. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/template/README.md +5 -0
  95. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/template/config.yaml +12 -0
  96. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/template/eval.py +12 -0
  97. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/template/train.py +45 -0
  98. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/recipe.yaml +73 -0
  99. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/template/README.md +6 -0
  100. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/template/config.yaml +12 -0
  101. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/template/eval.py +12 -0
  102. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/template/train.py +54 -0
  103. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/recipe.yaml +68 -0
  104. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/template/README.md +5 -0
  105. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/template/config.yaml +10 -0
  106. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/template/eval.py +12 -0
  107. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/template/train.py +53 -0
  108. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/recipe.yaml +66 -0
  109. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/template/README.md +4 -0
  110. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/template/config.yaml +9 -0
  111. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/template/eval.py +12 -0
  112. starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/template/train.py +41 -0
  113. starforge_core-0.1.6/starforge_core/recipes/catalog/verl/grpo/recipe.yaml +282 -0
  114. starforge_core-0.1.6/starforge_core/recipes/catalog/verl/grpo/template/README.md +87 -0
  115. starforge_core-0.1.6/starforge_core/recipes/catalog/verl/grpo/template/config.yaml +7 -0
  116. starforge_core-0.1.6/starforge_core/recipes/catalog/verl/grpo/template/eval.py +5 -0
  117. starforge_core-0.1.6/starforge_core/recipes/catalog/verl/sft/recipe.yaml +162 -0
  118. starforge_core-0.1.6/starforge_core/recipes/catalog/verl/sft/template/README.md +4 -0
  119. starforge_core-0.1.6/starforge_core/recipes/catalog/verl/sft/template/config.yaml +6 -0
  120. starforge_core-0.1.6/starforge_core/recipes/catalog/verl/sft/template/eval.py +5 -0
  121. starforge_core-0.1.6/starforge_core/recipes/model.py +983 -0
  122. starforge_core-0.1.6/starforge_core/reporter.py +187 -0
  123. starforge_core-0.1.6/starforge_core/sandbox.py +117 -0
  124. starforge_core-0.1.6/tests/test_architecture_boundaries.py +57 -0
  125. starforge_core-0.1.6/tests/test_artifacts.py +147 -0
  126. starforge_core-0.1.6/tests/test_capsule.py +66 -0
  127. starforge_core-0.1.6/tests/test_config_validate.py +156 -0
  128. starforge_core-0.1.6/tests/test_contract_env_golden.py +358 -0
  129. starforge_core-0.1.6/tests/test_contract_spec.py +197 -0
  130. starforge_core-0.1.6/tests/test_contract_topology.py +104 -0
  131. starforge_core-0.1.6/tests/test_custom_adapter.py +109 -0
  132. starforge_core-0.1.6/tests/test_datasets.py +259 -0
  133. starforge_core-0.1.6/tests/test_distributed_coordinator.py +113 -0
  134. starforge_core-0.1.6/tests/test_evalkit.py +175 -0
  135. starforge_core-0.1.6/tests/test_framework_adapters.py +167 -0
  136. starforge_core-0.1.6/tests/test_framework_observability.py +520 -0
  137. starforge_core-0.1.6/tests/test_framework_runtime.py +138 -0
  138. starforge_core-0.1.6/tests/test_launcher.py +238 -0
  139. starforge_core-0.1.6/tests/test_lifecycle_commands.py +135 -0
  140. starforge_core-0.1.6/tests/test_metrics.py +51 -0
  141. starforge_core-0.1.6/tests/test_nemo_rl_adapter.py +278 -0
  142. starforge_core-0.1.6/tests/test_package_data.py +35 -0
  143. starforge_core-0.1.6/tests/test_process_group_plan.py +66 -0
  144. starforge_core-0.1.6/tests/test_recipes.py +497 -0
  145. starforge_core-0.1.6/tests/test_reporter.py +135 -0
  146. starforge_core-0.1.6/tests/test_runner.py +172 -0
  147. starforge_core-0.1.6/tests/test_runtime_artifacts.py +176 -0
  148. starforge_core-0.1.6/tests/test_sandbox.py +76 -0
  149. starforge_core-0.1.6/tests/test_trl_adapter.py +159 -0
  150. starforge_core-0.1.6/tests/test_verl_adapter.py +357 -0
  151. starforge_core-0.1.6/uv.lock +254 -0
@@ -0,0 +1,36 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .python-version
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+
14
+ # 前端
15
+ web/node_modules/
16
+ web/dist/
17
+
18
+ # 本地数据库 / 凭据 / 台账
19
+ .forge/
20
+ *.db
21
+
22
+ # 密钥 / 本地配置
23
+ .env
24
+ .env.*
25
+ !.env.example
26
+ docker-compose.override.yml
27
+ *.key
28
+ secrets.*
29
+
30
+ # 系统 / 编辑器
31
+ .DS_Store
32
+ .idea/
33
+ .vscode/
34
+ *.swp
35
+ SwanLab/
36
+ RL/
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.5
2
+ Name: starforge-core
3
+ Version: 0.1.6
4
+ Summary: StarForge 平台契约内核:Console/CLI 共享 JobSpec 与 recipe,作业侧由 Job Capsule PEX 携带。
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: packaging>=24
7
+ Requires-Dist: pyyaml>=6
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=8; extra == 'dev'
10
+ Requires-Dist: ruff>=0.5; extra == 'dev'
@@ -0,0 +1,19 @@
1
+ core:test-and-build:
2
+ image: ghcr.io/astral-sh/uv:python3.12-bookworm-slim
3
+ script:
4
+ - uv run --project core --extra dev pytest -q core/tests
5
+ - uv run --project core --extra dev ruff check core
6
+ - bash core/scripts/build_and_check.sh core/dist
7
+ artifacts:
8
+ paths:
9
+ - core/dist/
10
+
11
+ core:publish:
12
+ image: ghcr.io/astral-sh/uv:python3.12-bookworm-slim
13
+ needs: [core:test-and-build]
14
+ rules:
15
+ - if: '$CI_COMMIT_TAG =~ /^core-v/'
16
+ script:
17
+ - test -n "$UV_PUBLISH_USERNAME"
18
+ - test -n "$UV_PUBLISH_PASSWORD"
19
+ - uv publish --publish-url "$NEXUS_PYPI_PUBLISH_URL" core/dist/starforge_core-*
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "starforge-core"
3
+ version = "0.1.6"
4
+ description = "StarForge 平台契约内核:Console/CLI 共享 JobSpec 与 recipe,作业侧由 Job Capsule PEX 携带。"
5
+ requires-python = ">=3.10"
6
+ # packaging 是 recipe runtime.requires 的强校验器;缺失时必须在启动阶段失败,
7
+ # 不能悄悄跳过兼容检查。上报仍走标准库 urllib,不引 requests。
8
+ dependencies = ["packaging>=24", "pyyaml>=6"]
9
+
10
+ [project.optional-dependencies]
11
+ dev = ["pytest>=8", "ruff>=0.5"]
12
+
13
+ [project.scripts]
14
+ # 集群侧作业入口:容器 CMD 直接调它,不再需要上传包里的 shell 脚本。
15
+ forge-launch = "starforge_core.launcher:main"
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["starforge_core"]
23
+
24
+ [tool.ruff]
25
+ line-length = 120
26
+ target-version = "py310"
27
+
28
+ [tool.ruff.lint]
29
+ select = ["E", "F", "I", "W", "B"]
30
+ ignore = ["E501", "B008"]
@@ -0,0 +1,3 @@
1
+ [pytest]
2
+ testpaths = tests
3
+ pythonpath = .
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ CORE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ OUT_DIR="${1:-${CORE_ROOT}/dist}"
6
+
7
+ VERSION="$(python3 - "${CORE_ROOT}/pyproject.toml" <<'PY'
8
+ import re, sys
9
+ text = open(sys.argv[1], encoding="utf-8").read()
10
+ match = re.search(r'(?m)^version\s*=\s*"([^"]+)"', text)
11
+ if not match:
12
+ raise SystemExit("core/pyproject.toml 里没有 version")
13
+ print(match.group(1))
14
+ PY
15
+ )"
16
+
17
+ mkdir -p "${OUT_DIR}"
18
+ uv build "${CORE_ROOT}" --out-dir "${OUT_DIR}"
19
+
20
+ WHEEL="$(find "${OUT_DIR}" -maxdepth 1 -name "starforge_core-${VERSION}-*.whl" -print -quit)"
21
+ if [[ -z "${WHEEL}" ]]; then
22
+ echo "starforge-core ${VERSION} wheel 未生成" >&2
23
+ exit 1
24
+ fi
25
+
26
+ python3 - "${WHEEL}" <<'PY'
27
+ import sys
28
+ import zipfile
29
+
30
+ with zipfile.ZipFile(sys.argv[1]) as archive:
31
+ names = set(archive.namelist())
32
+
33
+ required = {
34
+ "starforge_core/recipes/catalog/nemo-rl/grpo/template/config.yaml",
35
+ "starforge_core/recipes/catalog/verl/grpo/recipe.yaml",
36
+ "starforge_core/recipes/catalog/trl/grpo/recipe.yaml",
37
+ "starforge_core/recipes/catalog/custom/custom/recipe.yaml",
38
+ }
39
+ missing = sorted(required - names)
40
+ if missing:
41
+ raise SystemExit("core wheel 缺少 package data: " + ", ".join(missing))
42
+ PY
43
+ echo "core wheel verified: ${WHEEL}"
@@ -0,0 +1,80 @@
1
+ """starforge-core —— 平台与集群之间的契约内核。
2
+
3
+ 谁在用
4
+ ──────────────────────────────────────────────────────────────────────────────
5
+ console(控制面) 校验 JobSpec、按 recipe 目录做准入、装配作业
6
+ sf CLI(客户端) 构建 JobSpec、本地按 recipe schema 预校验
7
+ 训练镜像(集群侧) launcher 读 spec 决定怎么跑;reporter 回传平台事实
8
+
9
+ 为什么独立成包
10
+ ──────────────────────────────────────────────────────────────────────────────
11
+ 三方都只依赖这一个包,任何一方都不依赖另一方。若契约住在某个客户端包里,
12
+ 控制面就会反向依赖自己的客户端,客户端发版就可能牵动平台。
13
+
14
+ 契约的**所有权**归平台(源码在 console 仓),因为「支持哪些后训练方法、
15
+ 作业规格长什么样」是平台的决定,不是某个客户端的决定。
16
+
17
+ 依赖极简(仅 pyyaml,上报走标准库 urllib):本包要能在任何训练镜像里 import,
18
+ 少一个依赖就少一处「镜像里恰好没装」的失败点。
19
+ """
20
+ from __future__ import annotations
21
+
22
+ __version__ = "0.1.6"
23
+
24
+ from .contract import (
25
+ API_VERSION,
26
+ KIND_TRAINING,
27
+ KNOWN_KEYS,
28
+ SPEC_FILE_PATH,
29
+ ArtifactRef,
30
+ DistributedTopology,
31
+ IngestBinding,
32
+ JobSpec,
33
+ JobSpecBody,
34
+ LifecycleSpec,
35
+ Metadata,
36
+ ModelSpec,
37
+ PlatformBinding,
38
+ Provenance,
39
+ RecipeRef,
40
+ ResourcePool,
41
+ ResourceSpec,
42
+ SourceSpec,
43
+ SpecError,
44
+ compile_distributed_topology,
45
+ compile_pool_topologies,
46
+ spec_to_env,
47
+ )
48
+ from .recipes import Recipe, all_recipes, get_recipe, recipe_names
49
+ from .reporter import Reporter
50
+
51
+ __all__ = [
52
+ "API_VERSION",
53
+ "ArtifactRef",
54
+ "DistributedTopology",
55
+ "IngestBinding",
56
+ "JobSpec",
57
+ "JobSpecBody",
58
+ "KIND_TRAINING",
59
+ "KNOWN_KEYS",
60
+ "LifecycleSpec",
61
+ "Metadata",
62
+ "ModelSpec",
63
+ "PlatformBinding",
64
+ "Provenance",
65
+ "Recipe",
66
+ "RecipeRef",
67
+ "ResourcePool",
68
+ "ResourceSpec",
69
+ "Reporter",
70
+ "SPEC_FILE_PATH",
71
+ "SourceSpec",
72
+ "SpecError",
73
+ "compile_distributed_topology",
74
+ "compile_pool_topologies",
75
+ "all_recipes",
76
+ "get_recipe",
77
+ "recipe_names",
78
+ "spec_to_env",
79
+ "__version__",
80
+ ]
@@ -0,0 +1,140 @@
1
+ """Recipe-owned artifact discovery and ``forge/artifacts/v1`` manifests."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from starforge_core.recipes import ArtifactContract
10
+ from starforge_core.reporter import Reporter
11
+
12
+ MANIFEST_NAME = "artifacts.json"
13
+ _STEP_RE = re.compile(r"(?:^|/)(?:global_)?step_(\d+)(?:/|$)")
14
+
15
+
16
+ def _contained(root: Path, path: Path) -> Path:
17
+ resolved = path.resolve(strict=True)
18
+ if not resolved.is_relative_to(root):
19
+ raise ValueError(f"artifact glob 越过 FORGE_OUT_DIR: {path} -> {resolved}")
20
+ return resolved
21
+
22
+
23
+ def _size_bytes(root: Path, path: Path) -> int:
24
+ if path.is_file():
25
+ return path.stat().st_size
26
+ total = 0
27
+ for item in path.rglob("*"):
28
+ resolved = _contained(root, item)
29
+ if resolved.is_file():
30
+ total += resolved.stat().st_size
31
+ return total
32
+
33
+
34
+ def _step(relative: str) -> int | None:
35
+ match = _STEP_RE.search(relative)
36
+ return int(match.group(1)) if match else None
37
+
38
+
39
+ def _materialized_format(declared: str, path: Path) -> str:
40
+ if declared != "nemo-checkpoint":
41
+ return declared
42
+ weights = path / "policy" / "weights"
43
+ dcp = (weights / ".metadata").is_file()
44
+ megatron = [item for item in weights.glob("iter_*") if item.is_dir()] if weights.is_dir() else []
45
+ if dcp and not megatron:
46
+ return "nemo-dcp"
47
+ if len(megatron) == 1 and not dcp:
48
+ return "nemo-megatron"
49
+ raise ValueError(
50
+ f"NeMo checkpoint format 无法唯一确定: {path} "
51
+ f"(dcp_metadata={dcp}, megatron_iterations={len(megatron)})"
52
+ )
53
+
54
+
55
+ def _sort_key(item: dict[str, Any]) -> tuple[int, int, str]:
56
+ return (
57
+ 0 if item["kind"] == "checkpoint" else 1,
58
+ item.get("step") if item.get("step") is not None else -1,
59
+ item["path"],
60
+ )
61
+
62
+
63
+ def build_manifest(
64
+ output_root: Path,
65
+ *,
66
+ run_id: str,
67
+ framework: str,
68
+ recipe: str,
69
+ contract: ArtifactContract,
70
+ ) -> dict[str, Any]:
71
+ """Expand only declared globs and reject every realpath escape."""
72
+ root = output_root.expanduser().resolve(strict=True)
73
+ if contract.version != "forge/artifacts/v1":
74
+ raise ValueError(f"不支持的 artifact manifest 版本: {contract.version!r}")
75
+ groups = (
76
+ ("checkpoint", contract.checkpoints),
77
+ ("log", contract.logs),
78
+ ("hf_export", contract.exports),
79
+ ("eval_report", contract.evaluations),
80
+ )
81
+ artifacts: list[dict[str, Any]] = []
82
+ seen: set[tuple[str, str]] = set()
83
+ for kind, patterns in groups:
84
+ artifact_format = contract.formats.get(kind, "").strip()
85
+ if not artifact_format:
86
+ raise ValueError(f"artifact kind {kind!r} 缺少显式 format")
87
+ for pattern in patterns:
88
+ for match in root.glob(pattern):
89
+ resolved = _contained(root, match)
90
+ relative = match.relative_to(root).as_posix()
91
+ identity = (kind, relative)
92
+ if identity in seen:
93
+ continue
94
+ seen.add(identity)
95
+ item: dict[str, Any] = {
96
+ "kind": kind,
97
+ "path": relative,
98
+ "format": _materialized_format(artifact_format, resolved),
99
+ "size_bytes": _size_bytes(root, resolved),
100
+ }
101
+ if (step := _step(relative)) is not None:
102
+ item["step"] = step
103
+ artifacts.append(item)
104
+ artifacts.sort(key=_sort_key)
105
+ return {
106
+ "apiVersion": "forge/artifacts/v1",
107
+ "run_id": run_id,
108
+ "framework": framework,
109
+ "recipe": recipe,
110
+ "artifacts": artifacts,
111
+ }
112
+
113
+
114
+ def write_manifest(output_root: Path, manifest: dict[str, Any]) -> Path:
115
+ root = output_root.expanduser().resolve(strict=True)
116
+ path = root / MANIFEST_NAME
117
+ path.write_text(
118
+ json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
119
+ encoding="utf-8",
120
+ )
121
+ return path
122
+
123
+
124
+ def publish_manifest(
125
+ manifest: dict[str, Any],
126
+ *,
127
+ output_root: Path,
128
+ reporter: Reporter | None,
129
+ ) -> None:
130
+ if reporter is None:
131
+ return
132
+ root = output_root.expanduser().resolve(strict=True)
133
+ for item in manifest.get("artifacts", ()):
134
+ reporter.artifact(
135
+ item["kind"],
136
+ str(_contained(root, root / item["path"])),
137
+ step=item.get("step"),
138
+ size_bytes=item.get("size_bytes"),
139
+ format=item["format"],
140
+ )
@@ -0,0 +1,20 @@
1
+ """标准基准评测的 runner 抽象(lm-eval / evalscope 双后端)。"""
2
+ from __future__ import annotations
3
+
4
+ from .runners import (
5
+ BenchmarkRunner,
6
+ BenchmarkScore,
7
+ EvalscopeRunner,
8
+ LmEvalRunner,
9
+ RunnerError,
10
+ get_runner,
11
+ )
12
+
13
+ __all__ = [
14
+ "BenchmarkRunner",
15
+ "BenchmarkScore",
16
+ "EvalscopeRunner",
17
+ "LmEvalRunner",
18
+ "RunnerError",
19
+ "get_runner",
20
+ ]
@@ -0,0 +1,163 @@
1
+ """BenchmarkRunner 协议与两个一等实现(lm-eval / evalscope)。
2
+
3
+ 设计:
4
+ - runner = 「构造评测命令 + 解析分数产物」的适配器;进程执行、超时、
5
+ 产物落盘与上报由 lifecycle.benchmark_eval 统一负责。
6
+ - 分数统一归一成 BenchmarkScore(benchmark, metric, value)——console 的
7
+ benchmark_scores 表与看板端点按这三元组聚合,不关心后端差异。
8
+ - 解析失败显式抛 RunnerError(评测跑完但拿不到分数 = 作业失败,
9
+ 不允许静默产出空报告)。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import shlex
15
+ import sys
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Protocol
19
+
20
+
21
+ class RunnerError(RuntimeError):
22
+ """评测 runner 命令构造/产物解析失败。"""
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class BenchmarkScore:
27
+ benchmark: str
28
+ metric: str
29
+ value: float
30
+
31
+ def to_dict(self) -> dict:
32
+ return {"benchmark": self.benchmark, "metric": self.metric, "value": self.value}
33
+
34
+
35
+ class BenchmarkRunner(Protocol):
36
+ name: str
37
+
38
+ def build_argv(
39
+ self, *, model: str, suites: list[str], output_dir: Path,
40
+ batch_size: str, limit: int | None, extra_args: str,
41
+ ) -> list[str]: ...
42
+
43
+ def parse_scores(self, output_dir: Path) -> list[BenchmarkScore]: ...
44
+
45
+
46
+ def _finite(value) -> float | None:
47
+ try:
48
+ v = float(value)
49
+ except (TypeError, ValueError):
50
+ return None
51
+ return v if v == v and abs(v) != float("inf") else None
52
+
53
+
54
+ class LmEvalRunner:
55
+ """EleutherAI lm-evaluation-harness(学术标准基准的事实标准)。"""
56
+
57
+ name = "lm-eval"
58
+
59
+ def build_argv(
60
+ self, *, model: str, suites: list[str], output_dir: Path,
61
+ batch_size: str, limit: int | None, extra_args: str,
62
+ ) -> list[str]:
63
+ argv = [
64
+ sys.executable, "-m", "lm_eval",
65
+ "--model", "hf",
66
+ "--model_args", f"pretrained={model}",
67
+ "--tasks", ",".join(suites),
68
+ "--batch_size", batch_size or "auto",
69
+ "--output_path", str(output_dir),
70
+ "--log_samples",
71
+ ]
72
+ if limit:
73
+ argv += ["--limit", str(limit)]
74
+ if extra_args:
75
+ argv += shlex.split(extra_args)
76
+ return argv
77
+
78
+ def parse_scores(self, output_dir: Path) -> list[BenchmarkScore]:
79
+ """解析 lm-eval 的 results*.json(新版本落在 output_path 下的模型子目录)。"""
80
+ candidates = sorted(output_dir.rglob("results*.json"))
81
+ if not candidates:
82
+ raise RunnerError(f"lm-eval 未产出 results*.json(查 {output_dir})")
83
+ data = json.loads(candidates[-1].read_text(encoding="utf-8"))
84
+ results = data.get("results")
85
+ if not isinstance(results, dict) or not results:
86
+ raise RunnerError("lm-eval results 段为空")
87
+ scores: list[BenchmarkScore] = []
88
+ for task, metrics in results.items():
89
+ if not isinstance(metrics, dict):
90
+ continue
91
+ for key, raw in metrics.items():
92
+ # 键形如 "acc,none" / "exact_match,strict-match";跳过 stderr 与别名字段
93
+ metric = str(key).split(",", 1)[0]
94
+ if metric in ("alias",) or metric.endswith("_stderr"):
95
+ continue
96
+ if (value := _finite(raw)) is not None:
97
+ scores.append(BenchmarkScore(str(task), metric, value))
98
+ if not scores:
99
+ raise RunnerError("lm-eval results 未解析出任何数值指标")
100
+ return scores
101
+
102
+
103
+ class EvalscopeRunner:
104
+ """ModelScope evalscope(中文基准如 C-Eval/CMMLU 覆盖更好)。"""
105
+
106
+ name = "evalscope"
107
+
108
+ def build_argv(
109
+ self, *, model: str, suites: list[str], output_dir: Path,
110
+ batch_size: str, limit: int | None, extra_args: str,
111
+ ) -> list[str]:
112
+ argv = [
113
+ sys.executable, "-m", "evalscope.cli.cli", "eval",
114
+ "--model", model,
115
+ "--datasets", *suites,
116
+ "--work-dir", str(output_dir),
117
+ ]
118
+ if limit:
119
+ argv += ["--limit", str(limit)]
120
+ if extra_args:
121
+ argv += shlex.split(extra_args)
122
+ return argv
123
+
124
+ def parse_scores(self, output_dir: Path) -> list[BenchmarkScore]:
125
+ """解析 evalscope 的 reports/**/*.json(每数据集一份报告)。"""
126
+ reports = [
127
+ p for p in sorted(output_dir.rglob("*.json"))
128
+ if "reports" in p.parts
129
+ ]
130
+ if not reports:
131
+ raise RunnerError(f"evalscope 未产出 reports/**/*.json(查 {output_dir})")
132
+ scores: list[BenchmarkScore] = []
133
+ for path in reports:
134
+ try:
135
+ data = json.loads(path.read_text(encoding="utf-8"))
136
+ except ValueError:
137
+ continue
138
+ if not isinstance(data, dict):
139
+ continue
140
+ dataset = str(data.get("dataset_name") or data.get("name") or path.stem)
141
+ # 首选 metrics 数组(新版报告);回退顶层 score 字段。
142
+ metrics = data.get("metrics")
143
+ if isinstance(metrics, list):
144
+ for m in metrics:
145
+ if isinstance(m, dict) and (value := _finite(m.get("score"))) is not None:
146
+ scores.append(BenchmarkScore(dataset, str(m.get("name") or "score"), value))
147
+ elif (value := _finite(data.get("score"))) is not None:
148
+ scores.append(BenchmarkScore(dataset, "score", value))
149
+ if not scores:
150
+ raise RunnerError("evalscope 报告未解析出任何数值指标")
151
+ return scores
152
+
153
+
154
+ _RUNNERS = {r.name: r for r in (LmEvalRunner(), EvalscopeRunner())}
155
+
156
+
157
+ def get_runner(name: str) -> BenchmarkRunner:
158
+ try:
159
+ return _RUNNERS[name]
160
+ except KeyError as exc:
161
+ raise RunnerError(
162
+ f"未知评测 runner {name!r};可用: {', '.join(sorted(_RUNNERS))}"
163
+ ) from exc