Update app.py
Browse files
app.py
CHANGED
|
@@ -11,6 +11,7 @@ import gradio as gr
|
|
| 11 |
import pandas as pd
|
| 12 |
import matplotlib.pyplot as plt
|
| 13 |
from huggingface_hub import snapshot_download, hf_hub_download, HfApi
|
|
|
|
| 14 |
|
| 15 |
from riichienv import RiichiEnv, GameRule
|
| 16 |
from model3pLOCAL import load_model
|
|
@@ -24,7 +25,7 @@ HF_TOKEN = os.getenv("HF_TOKEN")
|
|
| 24 |
|
| 25 |
# 为当前节点生成唯一的 ID
|
| 26 |
WORKER_ID = os.getenv("WORKER_ID", str(uuid.uuid4())[:6])
|
| 27 |
-
REPORT_FILE_PREFIX = '
|
| 28 |
REPORT_FILE = f"{REPORT_FILE_PREFIX}_{WORKER_ID}.txt"
|
| 29 |
|
| 30 |
api = HfApi()
|
|
@@ -130,36 +131,48 @@ class MortalAgent:
|
|
| 130 |
return action
|
| 131 |
|
| 132 |
# ==========================================
|
| 133 |
-
# 2. 核心对局任务
|
| 134 |
# ==========================================
|
| 135 |
-
def
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
|
| 140 |
-
for
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
| 153 |
|
| 154 |
# ==========================================
|
| 155 |
# 3. 后台独立评估线程
|
| 156 |
# ==========================================
|
| 157 |
def background_eval_loop():
|
| 158 |
-
sync_models_from_hub() #
|
| 159 |
-
sync_data_from_hub() #
|
| 160 |
|
| 161 |
-
NUM_WORKERS =
|
| 162 |
-
print(f"🚀 节点 [{WORKER_ID}] 后台对战线程已启动: {TEST_MODEL} 挑战双 {EXAMINER_MODEL}")
|
| 163 |
|
| 164 |
if not os.path.exists(REPORT_FILE):
|
| 165 |
open(REPORT_FILE, 'w').close()
|
|
@@ -167,7 +180,8 @@ def background_eval_loop():
|
|
| 167 |
games_since_last_sync = 0
|
| 168 |
|
| 169 |
with concurrent.futures.ProcessPoolExecutor(max_workers=NUM_WORKERS) as executor:
|
| 170 |
-
|
|
|
|
| 171 |
games_completed = 0
|
| 172 |
|
| 173 |
while EVAL_RUNNING and futures:
|
|
@@ -178,19 +192,23 @@ def background_eval_loop():
|
|
| 178 |
with open(REPORT_FILE, "a") as f:
|
| 179 |
for future in done:
|
| 180 |
try:
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
f.flush()
|
| 184 |
-
games_completed += 1
|
| 185 |
-
games_since_last_sync += 1
|
| 186 |
-
print(f"[节点 {WORKER_ID}] 完成 {games_completed} 局: 顺位 {rank}, 得点 {score}")
|
| 187 |
except Exception as e:
|
| 188 |
print(f"对局异常: {e}")
|
| 189 |
|
| 190 |
if EVAL_RUNNING:
|
| 191 |
-
|
|
|
|
| 192 |
|
| 193 |
-
|
|
|
|
| 194 |
sync_data_to_hub()
|
| 195 |
sync_data_from_hub()
|
| 196 |
games_since_last_sync = 0
|
|
@@ -284,14 +302,7 @@ with gr.Blocks() as demo:
|
|
| 284 |
|
| 285 |
refresh_btn.click(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
|
| 286 |
|
| 287 |
-
import multiprocessing
|
| 288 |
-
|
| 289 |
if __name__ == "__main__":
|
| 290 |
-
# 强制使用 spawn,解决 Rust FFI 多进程死锁问题
|
| 291 |
-
multiprocessing.set_start_method('spawn', force=True)
|
| 292 |
-
|
| 293 |
-
# 修复后,你可以安全地将 NUM_WORKERS 调大
|
| 294 |
-
# HF Space 免费层通常有 2 vCPU,可以设为 2。如果是更高配置的机器,设为 os.cpu_count() - 1
|
| 295 |
t = threading.Thread(target=background_eval_loop, daemon=True)
|
| 296 |
t.start()
|
| 297 |
|
|
|
|
| 11 |
import pandas as pd
|
| 12 |
import matplotlib.pyplot as plt
|
| 13 |
from huggingface_hub import snapshot_download, hf_hub_download, HfApi
|
| 14 |
+
import secrets
|
| 15 |
|
| 16 |
from riichienv import RiichiEnv, GameRule
|
| 17 |
from model3pLOCAL import load_model
|
|
|
|
| 25 |
|
| 26 |
# 为当前节点生成唯一的 ID
|
| 27 |
WORKER_ID = os.getenv("WORKER_ID", str(uuid.uuid4())[:6])
|
| 28 |
+
REPORT_FILE_PREFIX = 'Step40800P42998_vs_9070_Fair_eval_report'
|
| 29 |
REPORT_FILE = f"{REPORT_FILE_PREFIX}_{WORKER_ID}.txt"
|
| 30 |
|
| 31 |
api = HfApi()
|
|
|
|
| 131 |
return action
|
| 132 |
|
| 133 |
# ==========================================
|
| 134 |
+
# 2. 核心对局任务 (复式评测:同种子 3 局轮换)
|
| 135 |
# ==========================================
|
| 136 |
+
def play_three_games_with_seed(seed):
|
| 137 |
+
"""
|
| 138 |
+
接收一个全局唯一的随机种子,让测试模型在 0, 1, 2 三个位置各打一局。
|
| 139 |
+
返回 3 局的成绩列表。
|
| 140 |
+
"""
|
| 141 |
+
results = []
|
| 142 |
|
| 143 |
+
# 轮换测试模型的位置
|
| 144 |
+
for test_seat in range(3):
|
| 145 |
+
env = RiichiEnv(game_mode="3p-red-half", rule=GameRule.default_tenhou())
|
| 146 |
+
agents = {}
|
| 147 |
+
for i in range(3):
|
| 148 |
+
# 测试模型坐在 test_seat,其他位置是考官模型
|
| 149 |
+
model_file = TEST_MODEL if i == test_seat else EXAMINER_MODEL
|
| 150 |
+
agents[i] = MortalAgent(i, model_file)
|
| 151 |
+
|
| 152 |
+
# 🚀 关键:注入指定的种子!这样三次循环生成的牌山是完全一致的
|
| 153 |
+
obs_dict = env.reset(seed=seed)
|
| 154 |
+
|
| 155 |
+
while not env.done():
|
| 156 |
+
actions = {pid: agents[pid].act(obs) for pid, obs in obs_dict.items()}
|
| 157 |
+
obs_dict = env.step(actions)
|
| 158 |
+
|
| 159 |
+
scores = env.scores()
|
| 160 |
+
ranks = env.ranks()
|
| 161 |
|
| 162 |
+
# 记录测试模型在这一局的表现
|
| 163 |
+
results.append((ranks[test_seat], scores[test_seat]))
|
| 164 |
+
|
| 165 |
+
return results
|
| 166 |
|
| 167 |
# ==========================================
|
| 168 |
# 3. 后台独立评估线程
|
| 169 |
# ==========================================
|
| 170 |
def background_eval_loop():
|
| 171 |
+
sync_models_from_hub() # 启动时从 Riichi-Model-Repo 拉取对战模型
|
| 172 |
+
sync_data_from_hub() # 启动时从战绩仓库拉取历史战绩
|
| 173 |
|
| 174 |
+
NUM_WORKERS = 1
|
| 175 |
+
print(f"🚀 节点 [{WORKER_ID}] 后台对战线程已启动: {TEST_MODEL} 挑战双 {EXAMINER_MODEL} (复式打法)")
|
| 176 |
|
| 177 |
if not os.path.exists(REPORT_FILE):
|
| 178 |
open(REPORT_FILE, 'w').close()
|
|
|
|
| 180 |
games_since_last_sync = 0
|
| 181 |
|
| 182 |
with concurrent.futures.ProcessPoolExecutor(max_workers=NUM_WORKERS) as executor:
|
| 183 |
+
# 🚀 使用 secrets.randbits(31) 生成跨节点绝对不冲突的 31 位正整数种子
|
| 184 |
+
futures = {executor.submit(play_three_games_with_seed, secrets.randbits(31)) for _ in range(NUM_WORKERS * 2)}
|
| 185 |
games_completed = 0
|
| 186 |
|
| 187 |
while EVAL_RUNNING and futures:
|
|
|
|
| 192 |
with open(REPORT_FILE, "a") as f:
|
| 193 |
for future in done:
|
| 194 |
try:
|
| 195 |
+
# 现在 future.result() 返回的是包含 3 局结果的列表
|
| 196 |
+
batch_results = future.result()
|
| 197 |
+
for rank, score in batch_results:
|
| 198 |
+
f.write(f"{rank} {score}\n")
|
| 199 |
+
games_completed += 1
|
| 200 |
+
games_since_last_sync += 1
|
| 201 |
+
print(f"[节点 {WORKER_ID}] 完成复式对局: 顺位 {rank}, 得点 {score}")
|
| 202 |
f.flush()
|
|
|
|
|
|
|
|
|
|
| 203 |
except Exception as e:
|
| 204 |
print(f"对局异常: {e}")
|
| 205 |
|
| 206 |
if EVAL_RUNNING:
|
| 207 |
+
# 完成了一轮复式,再投递一个全新的安全种子
|
| 208 |
+
futures.add(executor.submit(play_three_games_with_seed, secrets.randbits(31)))
|
| 209 |
|
| 210 |
+
# 由于一次产出 3 局,同步频率可以保持在累计完成 50 局左右时触发
|
| 211 |
+
if games_since_last_sync >= 60:
|
| 212 |
sync_data_to_hub()
|
| 213 |
sync_data_from_hub()
|
| 214 |
games_since_last_sync = 0
|
|
|
|
| 302 |
|
| 303 |
refresh_btn.click(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
|
| 304 |
|
|
|
|
|
|
|
| 305 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
t = threading.Thread(target=background_eval_loop, daemon=True)
|
| 307 |
t.start()
|
| 308 |
|