feat(tuning): release V0.8.2 Agent 界面重构
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled

This commit is contained in:
2026-09-03 16:25:53 +08:00
parent a9b07e0abf
commit 63d67a645b
35 changed files with 4847 additions and 811 deletions
+312 -28
View File
@@ -18,10 +18,12 @@ from .process import GpuLease, ResourceBusyError, terminate_process
from .schema import (
BASE_REWARD_CONFIGURATION,
merge_proposal,
validate_configuration_constraints,
validate_constraints,
validate_proposal,
)
from .scoring import DEFAULT_OBJECTIVE_WEIGHTS, score_evaluation, validate_objective_weights
from .storage import TuningStorage, now_iso
from .storage import StorageConflict, TuningStorage, now_iso
from .study import OptunaStudies
from .tensorboard import ingest_scalars
@@ -98,7 +100,7 @@ class TuningManager:
)
):
raise TuningError("gpuIds 必须是非空非负整数数组")
trial_count = self._integer(payload, "trialCount", 12, 4, 20)
trial_count = self._integer(payload, "trialCount", 12, 1, 100)
rung0 = self._integer(payload, "initialIterations", 300, 1, 1000000)
rung1 = self._integer(payload, "middleIterations", 900, rung0, 1000000)
rung2 = self._integer(payload, "finalIterations", 2000, rung1, 1000000)
@@ -159,6 +161,15 @@ class TuningManager:
session["trials"] = self.storage.list_trials(session_id)
session["proposals"] = self.storage.list_proposals(session_id)
session["audit"] = self.storage.audit_events(session_id)
control = self.storage.get_control(session_id)
current = next(
(trial for trial in session["trials"] if trial["id"] == session["currentTrialId"]),
None,
)
control["effectiveAfterCurrent"] = bool(
current and current["state"] in {"training", "evaluating"}
)
session["control"] = control
return session
def list(self) -> list[dict]:
@@ -274,8 +285,8 @@ class TuningManager:
policy = run_dir / "policy.onnx"
if return_code != 0 or checkpoint is None or not policy.is_file():
raise TuningError(f"训练失败(返回码 {return_code})或缺少 checkpoint/policy.onnx")
self._wait_if_paused(session_id, self.cancel_events[session_id])
# 暂停只关闭后续 Trial 调度门;已经开始的 Trial 必须连同固定评估一起
# 完成,避免把一次单步令牌错误地消耗在半个 Trial 上。
eval_output = run_dir / "evaluation.json"
eval_command = [
self.python,
@@ -332,6 +343,9 @@ class TuningManager:
score=scored["score"],
eligible=scored["eligible"],
)
best = self._best_highest_rung(session_id)
if best is not None:
self.storage.update_session(session_id, best_trial_id=best["id"])
try:
optuna_number = self.studies.record(
session_id,
@@ -363,6 +377,13 @@ class TuningManager:
default=None,
)
def _best_highest_rung(self, session_id: str) -> dict | None:
for rung in (2, 1, 0):
best = self._best(session_id, rung=rung)
if best is not None:
return best
return None
def _proposal_context(self, session: dict) -> dict:
trials = self.storage.list_trials(session["id"])[-12:]
rejected_feedback = [
@@ -374,6 +395,7 @@ class TuningManager:
"task": session["config"]["taskId"],
"objectiveWeights": session["objectiveWeights"],
"allowlist": "服务端将验证固定 schema;最多四项修改",
"parameterConstraints": self.storage.get_control(session["id"])["constraints"],
"rejectedFeedback": rejected_feedback,
"trials": [
{
@@ -414,6 +436,8 @@ class TuningManager:
"model": "optuna-fallback",
}
source = "fallback"
constraints = self.storage.get_control(session["id"])["constraints"]
result["patch"] = validate_proposal(result["patch"], previous, constraints)
proposal = self.storage.create_proposal(
session["id"],
base_trial_id,
@@ -487,7 +511,10 @@ class TuningManager:
if t["rung"] == 0 and t["state"] == "completed"
]
next_number = len({t["number"] for t in completed_rung0})
best_score = max((t["score"] or 0.0 for t in completed_rung0), default=0.0)
best_score = max(
(t["score"] if t["score"] is not None else float("-inf") for t in completed_rung0),
default=0.0,
)
no_improve = 0
while (
next_number < session["config"]["trialCount"]
@@ -495,12 +522,19 @@ class TuningManager:
):
if cancel.is_set():
raise TuningError("session 已取消")
self._wait_if_paused(session_id, cancel)
base = self._best(session_id, rung=0) or completed_rung0[0]
self._wait_for_dispatch(session_id, cancel)
control = self.storage.get_control(session_id)
base = (
self.storage.get_trial(control["activeBaseTrialId"])
if control["activeBaseTrialId"]
else self._best(session_id, rung=0) or completed_rung0[0]
)
proposal = self._request_proposal(
session, base["rewardConfig"], base["id"], next_number
)
if session["mode"] == "approval":
# 模式允许在 session 运行期间切换,因此每次决策都读取最新持久化值。
current_mode = self.storage.get_session(session_id)["mode"]
if current_mode == "approval":
self.storage.update_session(
session_id, state="awaiting_approval", message="等待批准 Agent 建议"
)
@@ -515,8 +549,9 @@ class TuningManager:
else:
self.storage.decide_proposal(proposal["id"], "approved", "自动模式")
proposal = self.storage.get_proposal(proposal["id"])
self._wait_if_paused(session_id, cancel)
reward_config = merge_proposal(base["rewardConfig"], proposal["patch"])
self._claim_dispatch(session_id, cancel)
constraints = self.storage.get_control(session_id)["constraints"]
reward_config = merge_proposal(base["rewardConfig"], proposal["patch"], constraints)
trial = self.storage.create_trial(
session_id,
next_number,
@@ -527,8 +562,16 @@ class TuningManager:
f"trial-{next_number:03d}-rung-0",
)
result = self._execute_trial(session, trial)
if result["eligible"] and (result["score"] or -999) > best_score + 0.01:
best_score = result["score"]
if control["activeBaseTrialId"]:
self.storage.set_active_base(session_id, None)
self._pause_after_step(session_id)
result_score = result["score"]
if (
result["eligible"]
and result_score is not None
and result_score > best_score + 0.01
):
best_score = result_score
no_improve = 0
else:
no_improve += 1
@@ -537,13 +580,17 @@ class TuningManager:
# Promote top configurations; each new rung resumes its own previous checkpoint.
for rung in (1, 2):
self._wait_if_paused(session_id, cancel)
previous = [
t
for t in self.storage.list_trials(session_id)
if t["rung"] == rung - 1 and t["state"] == "completed" and t["eligible"]
]
previous.sort(key=lambda t: t["score"] or -999, reverse=True)
previous.sort(
key=lambda trial: (
trial["score"] if trial["score"] is not None else float("-inf")
),
reverse=True,
)
promoted_numbers = {
trial["number"]
for trial in self.storage.list_trials(session_id)
@@ -554,6 +601,7 @@ class TuningManager:
continue
if cancel.is_set():
raise TuningError("session 已取消")
self._claim_dispatch(session_id, cancel)
root = self._session_root(session_id)
checkpoint = root / parent["checkpointPath"]
trial = self.storage.create_trial(
@@ -566,12 +614,9 @@ class TuningManager:
f"trial-{parent['number']:03d}-rung-{rung}",
)
self._execute_trial(session, trial, checkpoint)
self._pause_after_step(session_id)
best = (
self._best(session_id, rung=2)
or self._best(session_id, rung=1)
or self._best(session_id, rung=0)
)
best = self._best_highest_rung(session_id)
if best is None:
raise TuningError("没有通过安全门槛的 trial")
preset_name = f"{session['config']['runName']}-{session_id[:8]}"
@@ -602,10 +647,42 @@ class TuningManager:
self.workers.pop(session_id, None)
self.processes.pop(session_id, None)
def _wait_if_paused(self, session_id: str, cancel: threading.Event) -> None:
def _wait_for_dispatch(self, session_id: str, cancel: threading.Event) -> None:
"""Wait at a scheduler boundary without consuming a one-shot token."""
with self.condition:
while self.storage.get_session(session_id)["state"] == "paused" and not cancel.is_set():
while not cancel.is_set():
session = self.storage.get_session(session_id)
control = self.storage.get_control(session_id)
if session["state"] == "paused":
self.condition.wait(timeout=1.0)
continue
if control["runPolicy"] == "continuous" or control["dispatchTokens"] > 0:
return
self.storage.update_session(
session_id,
state="paused",
message="单步 Trial 已完成;等待下一个调度令牌",
)
self.storage.audit(session_id, "step_gate_waiting", {})
self.condition.wait(timeout=1.0)
raise TuningError("session 已取消")
def _claim_dispatch(self, session_id: str, cancel: threading.Event) -> None:
while not cancel.is_set():
self._wait_for_dispatch(session_id, cancel)
if self.storage.use_dispatch_token(session_id):
return
raise TuningError("session 已取消")
def _pause_after_step(self, session_id: str) -> None:
control = self.storage.get_control(session_id)
if control["runPolicy"] == "step" and control["dispatchTokens"] == 0:
self.storage.update_session(
session_id,
state="paused",
message="单步 Trial 已完成;后续调度已暂停",
)
self.storage.audit(session_id, "step_trial_completed", {})
def approve(self, session_id: str, proposal_id: str, payload: Any) -> dict:
proposal = self.storage.get_proposal(proposal_id)
@@ -613,11 +690,15 @@ class TuningManager:
raise TuningError("proposal 不属于该 session")
patch = proposal["patch"]
feedback = None
base = self.storage.get_trial(proposal["baseTrialId"])
constraints = self.storage.get_control(session_id)["constraints"]
if isinstance(payload, dict):
feedback = payload.get("feedback")
if "patch" in payload:
base = self.storage.get_trial(proposal["baseTrialId"])
patch = validate_proposal(payload["patch"], base["rewardConfig"])
patch = payload["patch"]
if feedback is not None and (not isinstance(feedback, str) or len(feedback) > 2000):
raise TuningError("feedback 无效")
patch = validate_proposal(patch, base["rewardConfig"], constraints)
if not self.storage.decide_proposal(proposal_id, "approved", feedback, patch):
raise TuningError("proposal 已处理")
self.storage.audit(
@@ -642,22 +723,213 @@ class TuningManager:
self.condition.notify_all()
return self.detail(session_id)
def set_mode(self, session_id: str, payload: Any) -> dict:
if not isinstance(payload, dict) or payload.get("mode") not in ("automatic", "approval"):
raise TuningError("mode 必须是 automatic 或 approval")
mode = payload["mode"]
with self.condition:
session = self.storage.get_session(session_id)
if session["state"] not in RUNNING_STATES | {"awaiting_approval", "paused"}:
raise TuningError("当前状态不能切换运行模式")
previous = session["mode"]
if previous == mode:
return self.detail(session_id)
self.storage.update_session(session_id, mode=mode)
approved_ids = []
if mode == "automatic":
constraints = self.storage.get_control(session_id)["constraints"]
for proposal in self.storage.list_proposals(session_id):
if proposal["state"] != "pending":
continue
base = self.storage.get_trial(proposal["baseTrialId"])
try:
validate_proposal(proposal["patch"], base["rewardConfig"], constraints)
except Exception as error:
self.storage.decide_proposal(
proposal["id"], "rejected", f"参数护栏已变化:{error}"
)
continue
if self.storage.decide_proposal(
proposal["id"], "approved", "运行时切换为全自动模式"
):
approved_ids.append(proposal["id"])
if session["state"] == "awaiting_approval":
self.storage.update_session(
session_id, state="running", message="已切换为全自动模式,继续调参"
)
self.storage.audit(
session_id,
"session_mode_changed",
{"from": previous, "to": mode, "autoApprovedProposalIds": approved_ids},
)
self.condition.notify_all()
return self.detail(session_id)
def set_constraints(self, session_id: str, payload: Any) -> dict:
if not isinstance(payload, dict) or set(payload) != {"revision", "constraints"}:
raise TuningError("参数护栏请求必须包含 revision 与 constraints")
revision = payload["revision"]
if isinstance(revision, bool) or not isinstance(revision, int) or revision < 0:
raise TuningError("constraints revision 必须是非负整数")
constraints = validate_constraints(payload["constraints"])
session = self.storage.get_session(session_id)
if session["state"] not in ACTIVE_SESSION_STATES:
raise TuningError("终态 session 不能修改参数护栏")
control = self.storage.get_control(session_id)
base = None
if control["activeBaseTrialId"]:
base = self.storage.get_trial(control["activeBaseTrialId"])
elif session["currentTrialId"]:
base = self.storage.get_trial(session["currentTrialId"])
else:
base = self._best_highest_rung(session_id)
if base is not None:
validate_configuration_constraints(base["rewardConfig"], constraints)
try:
updated = self.storage.replace_constraints(session_id, revision, constraints)
except StorageConflict as error:
raise ResourceBusyError(str(error)) from error
rejected = []
for proposal in self.storage.list_proposals(session_id):
if proposal["state"] != "pending":
continue
proposal_base = self.storage.get_trial(proposal["baseTrialId"])
try:
validate_proposal(proposal["patch"], proposal_base["rewardConfig"], constraints)
except Exception as error:
message = f"参数护栏 revision {updated['constraintsRevision']}:{error}"
if self.storage.decide_proposal(proposal["id"], "rejected", message):
rejected.append(proposal["id"])
self.storage.audit(
session_id,
"constraints_updated",
{
"revision": updated["constraintsRevision"],
"paths": sorted(constraints),
"rejectedProposalIds": rejected,
},
)
if rejected:
with self.condition:
self.condition.notify_all()
return self.detail(session_id)
def step(self, session_id: str, payload: Any) -> dict:
if not isinstance(payload, dict) or payload.get("count", 1) != 1:
raise TuningError("单步调度一次只能发放 1 个 Trial 令牌")
session = self.storage.get_session(session_id)
if session["state"] not in {"paused", "awaiting_approval"}:
raise TuningError("请先暂停或等待 Proposal 审批,再执行单步 Trial")
control = self.storage.get_control(session_id)
if control["dispatchTokens"] > 0:
raise TuningError("已有未消费的单步 Trial 令牌")
try:
self.storage.grant_dispatch_token(session_id)
except StorageConflict as error:
raise ResourceBusyError(str(error)) from error
if session["state"] == "paused":
has_pending = any(
proposal["state"] == "pending"
for proposal in self.storage.list_proposals(session_id)
)
self.storage.update_session(
session_id,
state="awaiting_approval" if has_pending else "running",
message="单步令牌已就绪;请审批 Proposal"
if has_pending
else "已授权执行一个 Trial",
)
self.storage.audit(session_id, "step_token_granted", {"count": 1})
with self.condition:
self.condition.notify_all()
return self.detail(session_id)
def rollback(self, session_id: str, payload: Any) -> dict:
if not isinstance(payload, dict):
raise TuningError("rollback 请求体必须是对象")
session = self.storage.get_session(session_id)
if session["state"] not in {"paused", "awaiting_approval"}:
raise TuningError("回滚只能在安全暂停或等待审批时执行")
target_id = payload.get("trialId")
if payload.get("target") == "best":
target_id = session["bestTrialId"] or (self._best_highest_rung(session_id) or {}).get(
"id"
)
if not isinstance(target_id, str):
raise TuningError("rollback 必须指定 trialId 或 target=best")
target = self.storage.get_trial(target_id)
if target["sessionId"] != session_id:
raise TuningError("rollback Trial 不属于该 session")
if target["state"] != "completed" or not target["eligible"]:
raise TuningError("只能回滚到已完成且通过安全门槛的 Trial")
checkpoint = payload.get("checkpoint", False)
if not isinstance(checkpoint, bool):
raise TuningError("checkpoint 必须是布尔值")
if checkpoint:
path_value = target.get("checkpointPath")
if not path_value:
raise TuningError("目标 Trial 没有可用 checkpoint")
root = self._session_root(session_id)
path = (root / path_value).resolve()
if not path.is_relative_to(root) or not path.is_file():
raise TuningError("目标 checkpoint 不存在或路径非法")
superseded = []
for proposal in self.storage.list_proposals(session_id):
if proposal["state"] == "pending" and self.storage.decide_proposal(
proposal["id"], "rejected", f"由回滚到 Trial {target_id[:8]} 取代"
):
superseded.append(proposal["id"])
self.storage.set_active_base(session_id, target_id)
self.storage.reset_step_gate(session_id)
self.storage.update_session(
session_id,
state="paused",
message=f"已回滚到 Trial {target['number']} / Rung {target['rung']};等待单步或继续",
)
self.storage.audit(
session_id,
"rollback_selected",
{
"trialId": target_id,
"checkpoint": checkpoint,
"supersededProposalIds": superseded,
},
)
with self.condition:
self.condition.notify_all()
return self.detail(session_id)
def pause(self, session_id: str) -> dict:
session = self.storage.get_session(session_id)
if session["state"] not in RUNNING_STATES | {"awaiting_approval"}:
raise TuningError("当前状态不能暂停")
# 以持久化调度门记录暂停意图,而不是只依赖 session.state。当前 Trial
# 的训练/评估会继续完成,下一次 _claim_dispatch 必须等待显式继续或单步。
self.storage.reset_step_gate(session_id)
self.storage.update_session(
session_id, state="paused", message="已暂停后续调度;当前子进程将完成"
session_id, state="paused", message="已暂停后续调度;在途 Trial(如有)将完整结束"
)
return self.detail(session_id)
def resume(self, session_id: str) -> dict:
session = self.storage.get_session(session_id)
if session["state"] == "paused":
self.storage.update_session(session_id, state="running", message="继续调参")
self.storage.set_run_policy(session_id, "continuous")
has_pending = any(
proposal["state"] == "pending"
for proposal in self.storage.list_proposals(session_id)
)
self.storage.update_session(
session_id,
state="awaiting_approval" if has_pending else "running",
message="请审批待处理 Proposal" if has_pending else "连续调参已恢复",
)
with self.condition:
self.condition.notify_all()
elif session["state"] == "interrupted":
self.storage.set_run_policy(session_id, "continuous")
self.storage.update_session(session_id, state="queued", message="从最近完整结果恢复")
self._start_worker(session_id, resume=True)
else:
@@ -665,7 +937,9 @@ class TuningManager:
return self.detail(session_id)
def cancel(self, session_id: str) -> dict:
self.storage.get_session(session_id)
session = self.storage.get_session(session_id)
if session["state"] not in ACTIVE_SESSION_STATES:
return self.detail(session_id)
self.storage.update_session(session_id, state="cancelled", message="正在取消")
event = self.cancel_events.get(session_id)
if event:
@@ -678,12 +952,22 @@ class TuningManager:
return self.detail(session_id)
def metrics(
self, session_id: str, trial_id: str, tags: list[str] | None, max_points: int
self,
session_id: str,
trial_id: str,
tags: list[str] | None,
max_points: int,
after_step: int | None = None,
) -> dict:
trial = self.storage.get_trial(trial_id)
if trial["sessionId"] != session_id:
raise TuningError("trial 不属于该 session")
return {"trialId": trial_id, "series": self.storage.metrics(trial_id, tags, max_points)}
series = self.storage.metrics(trial_id, tags, max_points, after_step)
next_step = max(
(point["step"] for item in series for point in item["points"]),
default=after_step,
)
return {"trialId": trial_id, "series": series, "nextStep": next_step}
def best_artifact(self, session_id: str) -> Path:
session = self.storage.get_session(session_id)