feat: add durable success-first modelhub agent
This commit is contained in:
@@ -25,8 +25,17 @@ from market_intelligence import (
|
||||
)
|
||||
from modelhub_client import DEFAULT_CAPACITY_STATE_PATH, ModelHubClient, ModelHubClientPool
|
||||
from outcome_tracker import DEFAULT_OUTCOMES_PATH, OutcomeTracker
|
||||
from official_capabilities import DEFAULT_OFFICIAL_CAPABILITIES_PATH
|
||||
from queue_cleanup import cleanup_certain_oom_tasks
|
||||
from routing_engine import DEFAULT_ROUTING_STATE_PATH
|
||||
from runner_common import DEFAULT_KEY_PATH, ensure_tokens
|
||||
from state_sync import (
|
||||
DEFAULT_BATCH_SIZE,
|
||||
DEFAULT_BRANCH,
|
||||
DEFAULT_REMOTE,
|
||||
StateGitSync,
|
||||
load_state_git_credentials,
|
||||
)
|
||||
from submission_claims import DEFAULT_CLAIMS_PATH
|
||||
from template_selector import TemplateSelector
|
||||
from version import AGENT_VERSION
|
||||
@@ -132,6 +141,33 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
default=os.getenv("MODELHUB_GPU_STRATEGY_STATE_PATH", str(DEFAULT_GPU_STRATEGY_PATH)),
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--routing-state-path",
|
||||
default=os.getenv("MODELHUB_ROUTING_STATE_PATH", str(DEFAULT_ROUTING_STATE_PATH)),
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--official-capabilities-path",
|
||||
default=os.getenv("MODELHUB_OFFICIAL_CAPABILITIES_PATH", str(DEFAULT_OFFICIAL_CAPABILITIES_PATH)),
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument("--state-sync", action="store_true", help=argparse.SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--state-sync-remote",
|
||||
default=os.getenv("MODELHUB_STATE_SYNC_REMOTE", DEFAULT_REMOTE),
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--state-sync-branch",
|
||||
default=os.getenv("MODELHUB_STATE_SYNC_BRANCH", DEFAULT_BRANCH),
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--state-sync-batch-size",
|
||||
type=int,
|
||||
default=int(os.getenv("MODELHUB_STATE_SYNC_BATCH_SIZE", str(DEFAULT_BATCH_SIZE))),
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument("--gpu-strategy-recent-window", type=int, default=1000, help=argparse.SUPPRESS)
|
||||
parser.add_argument("--gpu-strategy-min-long-samples", type=int, default=100, help=argparse.SUPPRESS)
|
||||
parser.add_argument(
|
||||
@@ -471,10 +507,53 @@ def run_poll_loop(
|
||||
outcome_tracker: OutcomeTracker | None = None,
|
||||
) -> dict[str, Any]:
|
||||
now = now or utc_now()
|
||||
state_sync: StateGitSync | None = getattr(base_args, "_state_sync_manager", None)
|
||||
if state_sync is None and bool(getattr(base_args, "state_sync", False)):
|
||||
state_sync = StateGitSync(
|
||||
project_root=Path(__file__).resolve().parent.parent,
|
||||
credentials=load_state_git_credentials(),
|
||||
remote=str(getattr(base_args, "state_sync_remote", DEFAULT_REMOTE)),
|
||||
branch=str(getattr(base_args, "state_sync_branch", DEFAULT_BRANCH)),
|
||||
batch_size=max(1, int(getattr(base_args, "state_sync_batch_size", DEFAULT_BATCH_SIZE) or DEFAULT_BATCH_SIZE)),
|
||||
log_fn=log,
|
||||
)
|
||||
try:
|
||||
state_sync.acquire_process_lock()
|
||||
except Exception as exc:
|
||||
state_sync.last_error = str(exc)
|
||||
state_sync.healthy = False
|
||||
else:
|
||||
state_sync.restore()
|
||||
base_args._state_sync_manager = state_sync
|
||||
if state_sync is not None:
|
||||
state_sync.write_readiness(
|
||||
ready=False,
|
||||
reason="startup_recovery" if state_sync.healthy else "state_sync_unhealthy",
|
||||
)
|
||||
hf_discovery = hf_discovery or HuggingFaceDiscovery(base_url=base_args.hf_base_url)
|
||||
modelhub_client = modelhub_client or _build_modelhub_client(base_args)
|
||||
template_selector = template_selector or TemplateSelector()
|
||||
|
||||
if state_sync is not None and state_sync.healthy:
|
||||
try:
|
||||
active_tasks = (
|
||||
modelhub_client.list_active_tasks_by_account()
|
||||
if hasattr(modelhub_client, "list_active_tasks_by_account")
|
||||
else []
|
||||
)
|
||||
recovery = state_sync.reconcile_active_tasks(active_tasks)
|
||||
if not state_sync.sync("startup"):
|
||||
log("[cycle] paused reason=state_sync_unhealthy")
|
||||
else:
|
||||
log(
|
||||
f"[state-recovery] active={recovery['active']} "
|
||||
f"reconciled={recovery['reconciled']} unresolved={recovery['unresolved']}"
|
||||
)
|
||||
except Exception as exc:
|
||||
state_sync.healthy = False
|
||||
state_sync.last_error = str(exc)
|
||||
log(f"[state-recovery] active_scan_failed reason={type(exc).__name__}: {exc}")
|
||||
|
||||
poll_runs_dir = Path(base_args.poll_runs_dir)
|
||||
poll_runs_dir.mkdir(parents=True, exist_ok=True)
|
||||
poll_run_dir = make_run_dir(poll_runs_dir, now)
|
||||
@@ -535,6 +614,17 @@ def run_poll_loop(
|
||||
break
|
||||
|
||||
cycles += 1
|
||||
if state_sync is not None and not state_sync.healthy:
|
||||
recovered = (
|
||||
state_sync.sync("retry")
|
||||
if state_sync._workspace is not None
|
||||
else state_sync.retry_restore()
|
||||
)
|
||||
if not recovered:
|
||||
state_sync.write_readiness(ready=False, reason="state_sync_unhealthy")
|
||||
log(f"[poll] cycle={cycles} sleep={base_args.idle_interval_seconds}s reason=state_sync_unhealthy")
|
||||
time.sleep(base_args.idle_interval_seconds)
|
||||
continue
|
||||
if hasattr(modelhub_client, "configure_capacity_probe"):
|
||||
modelhub_client.configure_capacity_probe(cycles)
|
||||
|
||||
@@ -678,6 +768,10 @@ def run_poll_loop(
|
||||
"architectureIncompatibleCount": cleanup_summary[
|
||||
"architectureIncompatibleCount"
|
||||
],
|
||||
"officialCapabilityInvalidCount": cleanup_summary.get(
|
||||
"officialCapabilityInvalidCount",
|
||||
0,
|
||||
),
|
||||
"oldOverflowCount": cleanup_summary["oldOverflowCount"],
|
||||
"cancelledCount": cleanup_summary["cancelledCount"],
|
||||
"policyCancelledRecorded": policy_cancelled_recorded,
|
||||
@@ -701,6 +795,20 @@ def run_poll_loop(
|
||||
)
|
||||
|
||||
if available_slots is not None and available_slots <= 0:
|
||||
if state_sync is not None:
|
||||
try:
|
||||
if cycles % 3 == 0 and hasattr(modelhub_client, "list_active_tasks_by_account"):
|
||||
state_sync.reconcile_active_tasks(modelhub_client.list_active_tasks_by_account())
|
||||
sync_ok = state_sync.sync("cycle_no_slots")
|
||||
state_sync.write_readiness(
|
||||
ready=sync_ok,
|
||||
reason=None if sync_ok else "state_sync_unhealthy",
|
||||
extra={"cycle": cycles},
|
||||
)
|
||||
except Exception as exc:
|
||||
state_sync.healthy = False
|
||||
state_sync.last_error = str(exc)
|
||||
state_sync.write_readiness(ready=False, reason="state_sync_unhealthy")
|
||||
log(f"[poll] cycle={cycles} sleep={base_args.poll_interval_seconds}s reason=no_available_slots")
|
||||
time.sleep(base_args.poll_interval_seconds)
|
||||
continue
|
||||
@@ -726,6 +834,34 @@ def run_poll_loop(
|
||||
f"stop={cycle_summary['stoppedReason']}"
|
||||
)
|
||||
|
||||
if state_sync is not None:
|
||||
try:
|
||||
if cycle_summary.get("submittedTotal", 0) > 0 or cycles % 3 == 0:
|
||||
active_tasks = (
|
||||
modelhub_client.list_active_tasks_by_account()
|
||||
if hasattr(modelhub_client, "list_active_tasks_by_account")
|
||||
else []
|
||||
)
|
||||
state_sync.reconcile_active_tasks(active_tasks)
|
||||
sync_ok = state_sync.sync("cycle")
|
||||
official_paused = any(
|
||||
bool((wave_result.get("summary") or {}).get("paused"))
|
||||
for wave_result in (cycle_summary.get("waveResults") or [])
|
||||
)
|
||||
state_sync.write_readiness(
|
||||
ready=sync_ok and not official_paused,
|
||||
reason=(
|
||||
"critical_official_signal_unavailable"
|
||||
if official_paused
|
||||
else (None if sync_ok else "state_sync_unhealthy")
|
||||
),
|
||||
extra={"cycle": cycles},
|
||||
)
|
||||
except Exception as exc:
|
||||
state_sync.healthy = False
|
||||
state_sync.last_error = str(exc)
|
||||
state_sync.write_readiness(ready=False, reason="state_sync_unhealthy")
|
||||
|
||||
if base_args.daily_target > 0 and remaining_before_run is not None and remaining_before_run <= 0:
|
||||
stopped_reason = "daily_target_already_reached"
|
||||
break
|
||||
@@ -791,6 +927,9 @@ def run_poll_loop(
|
||||
}
|
||||
write_json(poll_run_dir / "summary.json", summary)
|
||||
log(f"[poll] finished submitted_total={submitted_total} cycles={cycles} stopped_reason={stopped_reason}")
|
||||
if state_sync is not None:
|
||||
state_sync.write_readiness(ready=False, reason="worker_stopped")
|
||||
state_sync.close()
|
||||
return summary
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user