ziyuzhou02 commited on
Commit
8c1bce7
·
verified ·
1 Parent(s): e317359

Deploy GitHub main 0d2efcf4a46b

Browse files
cloud/acceptance.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run an optional real model acceptance, isolated from production scores/state."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timezone
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ import signal
10
+ import sys
11
+ import tempfile
12
+ import time
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+ sys.path[:0] = [str(ROOT), str(ROOT / "src")]
16
+ BRANCH = "acceptance"
17
+
18
+
19
+ def publish(api, repo, output, report):
20
+ from huggingface_hub import CommitOperationAdd
21
+ from tsfm_bench.remote_state import pack_files
22
+ if not api.dataset_info(repo).private:
23
+ raise ValueError("Acceptance artifacts require a private Dataset")
24
+ api.create_branch(repo, branch=BRANCH, repo_type="dataset", exist_ok=True)
25
+ parent = api.dataset_info(repo, revision=BRANCH).sha
26
+ prefix = "acceptance/" + output.name
27
+ with tempfile.TemporaryDirectory(prefix="livehouse-acceptance-") as temp:
28
+ archive = Path(temp) / "artifacts.tar.gz"
29
+ manifest = pack_files(ROOT, sorted(p for p in output.rglob("*") if p.is_file()), archive)
30
+ return api.create_commit(repo, repo_type="dataset", revision=BRANCH, parent_commit=parent,
31
+ commit_message=f"Cloud model acceptance {output.name}: {report['status']}", operations=[
32
+ CommitOperationAdd(path_in_repo=prefix + "/artifacts.tar.gz", path_or_fileobj=archive),
33
+ CommitOperationAdd(path_in_repo=prefix + "/manifest.json", path_or_fileobj=json.dumps(manifest).encode()),
34
+ CommitOperationAdd(path_in_repo=prefix + "/report.json", path_or_fileobj=json.dumps(report, indent=2).encode()),
35
+ ])
36
+
37
+
38
+ def main():
39
+ from huggingface_hub import HfApi, hf_hub_download
40
+ from scripts.run_online_daemon import run_managed_subprocess
41
+ from tsfm_bench.remote_state import restore_archive
42
+ identifier = os.environ["LIVEHOUSE_ACCEPTANCE_ID"]
43
+ if not re.fullmatch(r"[a-z0-9][a-z0-9-]{1,60}", identifier):
44
+ raise ValueError("Acceptance ID must contain only lowercase letters, digits and hyphens")
45
+ signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
46
+ repo = os.environ["HF_STATE_REPO"]
47
+ token = os.environ["HF_TOKEN"]
48
+ api = HfApi(token=token)
49
+ if not api.dataset_info(repo).private:
50
+ raise ValueError("Acceptance artifacts require a private Dataset")
51
+ output = ROOT / "outputs/cloud-acceptance" / identifier
52
+ output.mkdir(parents=True, exist_ok=True)
53
+ prefix = "acceptance/" + identifier
54
+ api.create_branch(repo, branch=BRANCH, repo_type="dataset", exist_ok=True)
55
+ revision = api.dataset_info(repo, revision=BRANCH).sha
56
+ def download(name):
57
+ return Path(hf_hub_download(repo, prefix + "/" + name, repo_type="dataset",
58
+ revision=revision, token=token))
59
+ report = {"id":identifier, "isolated":True, "space":os.getenv("SPACE_ID"),
60
+ "source_revision":os.environ["LIVEHOUSE_SOURCE_REVISION"],
61
+ "started_at":datetime.now(timezone.utc).isoformat()}
62
+ if api.file_exists(repo, prefix + "/report.json", repo_type="dataset", revision=revision):
63
+ report = json.loads(download("report.json").read_text())
64
+ if report["status"] in {"complete", "failed"}:
65
+ print(f"Acceptance {identifier} already {report['status']}; no inference repeated", flush=True)
66
+ if report["status"] == "failed":
67
+ raise RuntimeError("This acceptance already failed; inspect its archived report")
68
+ return
69
+ manifest = json.loads(download("manifest.json").read_text())
70
+ restore_archive(ROOT, download("artifacts.tar.gz"), manifest, allowed_roots=(output.relative_to(ROOT),))
71
+ def run(phase):
72
+ return run_managed_subprocess([sys.executable, "scripts/live_acceptance.py", phase,
73
+ "--output-root", str(output)], timeout=1200, timeout_message="Cloud acceptance timed out")
74
+ try:
75
+ if report.get("status") != "issued":
76
+ if run("issue") != 0:
77
+ raise RuntimeError("At least one real model forecast failed; inspect archived acceptance-issue.json")
78
+ report.update(status="issued", issue=json.loads((output / "acceptance-issue.json").read_text()))
79
+ publish(api, repo, output, report)
80
+ # The target bar starts at the forecast deadline and closes five minutes later.
81
+ deadline = datetime.fromisoformat(report["issue"]["forecast_deadline"]).timestamp() + 310
82
+ while time.time() < deadline:
83
+ time.sleep(min(15, deadline - time.time()))
84
+ for attempt in range(4):
85
+ if run("resolve") == 0:
86
+ break
87
+ if attempt == 3:
88
+ raise RuntimeError("Real future observations did not resolve every model")
89
+ time.sleep(30)
90
+ report.update(status="complete", resolve=json.loads((output / "acceptance-resolve.json").read_text()))
91
+ except Exception as exc:
92
+ report.update(status="failed", error_type=type(exc).__name__)
93
+ report["finished_at"] = datetime.now(timezone.utc).isoformat()
94
+ publish(api, repo, output, report)
95
+ print(f"Cloud acceptance {identifier}: {report['status']}; artifacts on private Dataset branch {BRANCH}", flush=True)
96
+ if report["status"] != "complete":
97
+ raise RuntimeError("Cloud model acceptance failed; inspect its private artifacts")
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()
cloud/entrypoint.py CHANGED
@@ -44,6 +44,9 @@ def main():
44
  commands = {"web": [sys.executable, "cloud/web.py"]}
45
  if enabled:
46
  commands["worker"] = [sys.executable, "cloud/worker.py"]
 
 
 
47
  stopping = False
48
 
49
  def shutdown(*_):
@@ -61,10 +64,15 @@ def main():
61
  for name, command in commands.items():
62
  proc = processes.get(name)
63
  if proc is not None and proc.poll() is not None:
 
 
 
 
 
64
  print(f"{name} exited with {proc.returncode}; restart after 60 seconds", flush=True)
65
  restart_at[name] = time.monotonic() + 60
66
  del processes[name]
67
- if name not in processes and time.monotonic() >= restart_at.get(name, 0):
68
  processes[name] = subprocess.Popen(command, cwd=ROOT)
69
  status = {"updated_at": time.time(), "source_revision": revision,
70
  "evaluator_enabled": enabled,
 
44
  commands = {"web": [sys.executable, "cloud/web.py"]}
45
  if enabled:
46
  commands["worker"] = [sys.executable, "cloud/worker.py"]
47
+ if os.getenv("LIVEHOUSE_ACCEPTANCE_ID"):
48
+ commands["acceptance"] = [sys.executable, "cloud/acceptance.py"]
49
+ completed = set()
50
  stopping = False
51
 
52
  def shutdown(*_):
 
64
  for name, command in commands.items():
65
  proc = processes.get(name)
66
  if proc is not None and proc.poll() is not None:
67
+ if name == "acceptance":
68
+ print(f"Acceptance process finished with {proc.returncode}; no automatic rerun", flush=True)
69
+ completed.add(name)
70
+ del processes[name]
71
+ continue
72
  print(f"{name} exited with {proc.returncode}; restart after 60 seconds", flush=True)
73
  restart_at[name] = time.monotonic() + 60
74
  del processes[name]
75
+ if name not in processes and name not in completed and time.monotonic() >= restart_at.get(name, 0):
76
  processes[name] = subprocess.Popen(command, cwd=ROOT)
77
  status = {"updated_at": time.time(), "source_revision": revision,
78
  "evaluator_enabled": enabled,
deployment.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
  "source_repository": "https://github.com/Thinkcat-Lab/LiveHouse-TS",
3
  "source_branch": "main",
4
- "source_revision": "3feb6cda151162e151892022bd39aa5615f0256e"
5
  }
 
1
  {
2
  "source_repository": "https://github.com/Thinkcat-Lab/LiveHouse-TS",
3
  "source_branch": "main",
4
+ "source_revision": "0d2efcf4a46bffe86cd0bf965a279c46cb2ecc27"
5
  }
scripts/configure_cloud_space.py CHANGED
@@ -3,6 +3,7 @@
3
  from __future__ import annotations
4
 
5
  import os
 
6
  from pathlib import Path
7
 
8
 
@@ -17,6 +18,9 @@ def configure(api, env):
17
  enabled = env.get("TSFM_EVALUATOR_ENABLED", "0")
18
  if enabled not in {"0", "1"}:
19
  raise ValueError("TSFM_EVALUATOR_ENABLED must be 0 or 1")
 
 
 
20
  # Validate all destinations before the first secret/variable write.
21
  api.space_info(repo)
22
  if not api.dataset_info(state).private:
@@ -28,7 +32,7 @@ def configure(api, env):
28
  for name in names:
29
  api.add_space_secret(repo, name, env[name])
30
  variables = {"HF_SPACE_REPO":repo, "HF_RESULTS_REPO":results, "HF_STATE_REPO":state,
31
- "TSFM_EVALUATOR_ENABLED":enabled}
32
  for name, value in variables.items():
33
  api.add_space_variable(repo, name, value)
34
  return {"space":repo, "secret_names":list(names), "variables":variables}
 
3
  from __future__ import annotations
4
 
5
  import os
6
+ import re
7
  from pathlib import Path
8
 
9
 
 
18
  enabled = env.get("TSFM_EVALUATOR_ENABLED", "0")
19
  if enabled not in {"0", "1"}:
20
  raise ValueError("TSFM_EVALUATOR_ENABLED must be 0 or 1")
21
+ acceptance = env.get("LIVEHOUSE_ACCEPTANCE_ID", "")
22
+ if acceptance and not re.fullmatch(r"[a-z0-9][a-z0-9-]{1,60}", acceptance):
23
+ raise ValueError("Invalid cloud acceptance ID")
24
  # Validate all destinations before the first secret/variable write.
25
  api.space_info(repo)
26
  if not api.dataset_info(state).private:
 
32
  for name in names:
33
  api.add_space_secret(repo, name, env[name])
34
  variables = {"HF_SPACE_REPO":repo, "HF_RESULTS_REPO":results, "HF_STATE_REPO":state,
35
+ "TSFM_EVALUATOR_ENABLED":enabled, "LIVEHOUSE_ACCEPTANCE_ID":acceptance}
36
  for name, value in variables.items():
37
  api.add_space_variable(repo, name, value)
38
  return {"space":repo, "secret_names":list(names), "variables":variables}
scripts/live_acceptance.py CHANGED
@@ -56,7 +56,7 @@ def main():
56
  if remaining < 180:
57
  print(f"Waiting {remaining + 3:.0f}s for a full forecast issue window", flush=True)
58
  time.sleep(remaining + 3)
59
- response = requests.get("https://api.binance.com/api/v3/klines", timeout=45,
60
  params={"symbol": "BTCUSDT", "interval": "5m", "limit": 130})
61
  response.raise_for_status()
62
  rows = response.json()
 
56
  if remaining < 180:
57
  print(f"Waiting {remaining + 3:.0f}s for a full forecast issue window", flush=True)
58
  time.sleep(remaining + 3)
59
+ response = requests.get("https://data-api.binance.vision/api/v3/klines", timeout=45,
60
  params={"symbol": "BTCUSDT", "interval": "5m", "limit": 130})
61
  response.raise_for_status()
62
  rows = response.json()