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.
- starforge_core-0.1.6/.gitignore +36 -0
- starforge_core-0.1.6/PKG-INFO +10 -0
- starforge_core-0.1.6/gitlab-ci.example.yml +19 -0
- starforge_core-0.1.6/pyproject.toml +30 -0
- starforge_core-0.1.6/pytest.ini +3 -0
- starforge_core-0.1.6/scripts/build_and_check.sh +43 -0
- starforge_core-0.1.6/starforge_core/__init__.py +80 -0
- starforge_core-0.1.6/starforge_core/artifacts.py +140 -0
- starforge_core-0.1.6/starforge_core/benchmarks/__init__.py +20 -0
- starforge_core-0.1.6/starforge_core/benchmarks/runners.py +163 -0
- starforge_core-0.1.6/starforge_core/capsule.py +294 -0
- starforge_core-0.1.6/starforge_core/capsule_runner.py +100 -0
- starforge_core-0.1.6/starforge_core/config_resolve.py +391 -0
- starforge_core-0.1.6/starforge_core/contract/__init__.py +78 -0
- starforge_core-0.1.6/starforge_core/contract/binding.py +104 -0
- starforge_core-0.1.6/starforge_core/contract/env.py +193 -0
- starforge_core-0.1.6/starforge_core/contract/errors.py +18 -0
- starforge_core-0.1.6/starforge_core/contract/names.py +88 -0
- starforge_core-0.1.6/starforge_core/contract/spec.py +695 -0
- starforge_core-0.1.6/starforge_core/contract/topology.py +137 -0
- starforge_core-0.1.6/starforge_core/datasets.py +259 -0
- starforge_core-0.1.6/starforge_core/distributed/__init__.py +6 -0
- starforge_core-0.1.6/starforge_core/distributed/coordinator.py +148 -0
- starforge_core-0.1.6/starforge_core/distributed/protocol.py +52 -0
- starforge_core-0.1.6/starforge_core/distributed/ray_runtime.py +157 -0
- starforge_core-0.1.6/starforge_core/frameworks/__init__.py +24 -0
- starforge_core-0.1.6/starforge_core/frameworks/actions.py +61 -0
- starforge_core-0.1.6/starforge_core/frameworks/base.py +377 -0
- starforge_core-0.1.6/starforge_core/frameworks/custom.py +46 -0
- starforge_core-0.1.6/starforge_core/frameworks/evalkit.py +59 -0
- starforge_core-0.1.6/starforge_core/frameworks/nemo_rl.py +214 -0
- starforge_core-0.1.6/starforge_core/frameworks/observability.py +624 -0
- starforge_core-0.1.6/starforge_core/frameworks/registry.py +53 -0
- starforge_core-0.1.6/starforge_core/frameworks/runtime.py +164 -0
- starforge_core-0.1.6/starforge_core/frameworks/trl.py +161 -0
- starforge_core-0.1.6/starforge_core/frameworks/verl.py +274 -0
- starforge_core-0.1.6/starforge_core/launcher.py +410 -0
- starforge_core-0.1.6/starforge_core/lifecycle/__init__.py +2 -0
- starforge_core-0.1.6/starforge_core/lifecycle/benchmark_eval.py +107 -0
- starforge_core-0.1.6/starforge_core/lifecycle/export_complete.py +32 -0
- starforge_core-0.1.6/starforge_core/lifecycle/nemo_eval.py +65 -0
- starforge_core-0.1.6/starforge_core/lifecycle/nemo_export.py +105 -0
- starforge_core-0.1.6/starforge_core/lifecycle/verl_eval.py +197 -0
- starforge_core-0.1.6/starforge_core/metrics.py +75 -0
- starforge_core-0.1.6/starforge_core/package_bootstrap.py +73 -0
- starforge_core-0.1.6/starforge_core/pkce.py +39 -0
- starforge_core-0.1.6/starforge_core/plugins.py +311 -0
- starforge_core-0.1.6/starforge_core/recipes/__init__.py +188 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/custom/custom/recipe.yaml +45 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/custom/custom/template/README.md +9 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/custom/custom/template/train.sh +13 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/evalkit/benchmark/recipe.yaml +84 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/evalkit/benchmark/template/README.md +7 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/distillation/recipe.yaml +243 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/distillation/template/README.md +3 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/distillation/template/config.yaml +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/dpo/recipe.yaml +207 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/dpo/template/README.md +3 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/dpo/template/config.yaml +2 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo/recipe.yaml +467 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo/template/README.md +21 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo/template/config.yaml +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo-lora/recipe.yaml +185 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo-lora/template/README.md +5 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/grpo-lora/template/config.yaml +10 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/maxrl/recipe.yaml +322 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/maxrl/template/README.md +3 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/maxrl/template/config.yaml +6 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/maxrl/template/run.py +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/opsd/recipe.yaml +237 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/opsd/template/README.md +3 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/opsd/template/config.yaml +6 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/opsd/template/run.py +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/ppo/recipe.yaml +287 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/ppo/template/README.md +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/ppo/template/config.yaml +5 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/rm/recipe.yaml +177 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/rm/template/README.md +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/rm/template/config.yaml +5 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/sft/recipe.yaml +202 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/sft/template/README.md +3 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/nemo-rl/sft/template/config.yaml +34 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/recipe.yaml +70 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/template/README.md +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/template/config.yaml +10 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/template/eval.py +12 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/dpo/template/train.py +41 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/recipe.yaml +80 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/template/README.md +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/template/config.yaml +12 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/template/eval.py +12 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/grpo/template/train.py +54 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/recipe.yaml +71 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/template/README.md +5 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/template/config.yaml +12 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/template/eval.py +12 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/kto/template/train.py +45 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/recipe.yaml +73 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/template/README.md +6 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/template/config.yaml +12 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/template/eval.py +12 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rloo/template/train.py +54 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/recipe.yaml +68 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/template/README.md +5 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/template/config.yaml +10 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/template/eval.py +12 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/rm/template/train.py +53 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/recipe.yaml +66 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/template/README.md +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/template/config.yaml +9 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/template/eval.py +12 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/trl/sft/template/train.py +41 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/verl/grpo/recipe.yaml +282 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/verl/grpo/template/README.md +87 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/verl/grpo/template/config.yaml +7 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/verl/grpo/template/eval.py +5 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/verl/sft/recipe.yaml +162 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/verl/sft/template/README.md +4 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/verl/sft/template/config.yaml +6 -0
- starforge_core-0.1.6/starforge_core/recipes/catalog/verl/sft/template/eval.py +5 -0
- starforge_core-0.1.6/starforge_core/recipes/model.py +983 -0
- starforge_core-0.1.6/starforge_core/reporter.py +187 -0
- starforge_core-0.1.6/starforge_core/sandbox.py +117 -0
- starforge_core-0.1.6/tests/test_architecture_boundaries.py +57 -0
- starforge_core-0.1.6/tests/test_artifacts.py +147 -0
- starforge_core-0.1.6/tests/test_capsule.py +66 -0
- starforge_core-0.1.6/tests/test_config_validate.py +156 -0
- starforge_core-0.1.6/tests/test_contract_env_golden.py +358 -0
- starforge_core-0.1.6/tests/test_contract_spec.py +197 -0
- starforge_core-0.1.6/tests/test_contract_topology.py +104 -0
- starforge_core-0.1.6/tests/test_custom_adapter.py +109 -0
- starforge_core-0.1.6/tests/test_datasets.py +259 -0
- starforge_core-0.1.6/tests/test_distributed_coordinator.py +113 -0
- starforge_core-0.1.6/tests/test_evalkit.py +175 -0
- starforge_core-0.1.6/tests/test_framework_adapters.py +167 -0
- starforge_core-0.1.6/tests/test_framework_observability.py +520 -0
- starforge_core-0.1.6/tests/test_framework_runtime.py +138 -0
- starforge_core-0.1.6/tests/test_launcher.py +238 -0
- starforge_core-0.1.6/tests/test_lifecycle_commands.py +135 -0
- starforge_core-0.1.6/tests/test_metrics.py +51 -0
- starforge_core-0.1.6/tests/test_nemo_rl_adapter.py +278 -0
- starforge_core-0.1.6/tests/test_package_data.py +35 -0
- starforge_core-0.1.6/tests/test_process_group_plan.py +66 -0
- starforge_core-0.1.6/tests/test_recipes.py +497 -0
- starforge_core-0.1.6/tests/test_reporter.py +135 -0
- starforge_core-0.1.6/tests/test_runner.py +172 -0
- starforge_core-0.1.6/tests/test_runtime_artifacts.py +176 -0
- starforge_core-0.1.6/tests/test_sandbox.py +76 -0
- starforge_core-0.1.6/tests/test_trl_adapter.py +159 -0
- starforge_core-0.1.6/tests/test_verl_adapter.py +357 -0
- 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,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
|