diff --git a/Dockerfile b/Dockerfile index 65e820b..4a03d6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,8 @@ ENV EXTERNAL_SERVICE_TOKEN=4c44b92223c64bca9ed67701ceb2f4e8 ENV XC_TOKEN=4c44b92223c64bca9ed67701ceb2f4e8 ENV XC_TOKEN_FILE=/app/secrets/xc_token ENV ALL_TASKS_FOOTHOLD=1 +ENV LIVE_MAX_SUBMITS_PER_CYCLE=6 +ENV LIVE_MAX_ATTEMPTS_PER_CYCLE=30 EXPOSE 8080 diff --git a/README.md b/README.md index c1e3aec..ef0b4b5 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,12 @@ Tag `v0.2.3-live-token` 内置个人凭证,风险与停交说明见 [token bak Victor 的确认已内置,`LIVE_CONFIRM` 仍接受但不再必需。每次 task/add 前, 通过 Xc-Token 全量分页读取 onlyMine task/page;非终态计入队列,读取失败停交。 -`LIVE_QUEUE_THRESHOLD=95`(严格小于),`LIVE_MAX_SUBMITS_PER_CYCLE=3`, +`LIVE_QUEUE_THRESHOLD=95`(严格小于),`LIVE_MAX_SUBMITS_PER_CYCLE=6`(每小时 6 个;`LIVE_MAX_ATTEMPTS_PER_CYCLE=30`,60027/60028 及 code≠0 不计名额), `LIVE_CYCLE_SECONDS=3600`(最小 60 秒)。环境队列提示仅可额外阻塞。 候选来自 `PENDING_SUBMIT_MD` / `PENDING_SUBMIT_TXT`,否则使用 `src/data/candidates.json`(随镜像内置);A verified_reuse → 其他 A → B。 禁提、重复、GPU 未映射、占位符候选跳过。按 try_next_gpus 顺序选择首个已映射且未提交的模型/GPU 对。去重合并远端全部状态的 model/GPU、内置 `src/data/submitted-ledger.json` 和进程内提交记录;永久拒绝对内置于 `src/data/reject-marks.json`,不依赖容器文件。A 允许同模型换 GPU;do-not-resubmit 模型跨 GPU 全禁。服务启动记录 remote/baked/marks 数量;每次提交前刷新远端,失败则 hold `ledger_rebuild_failed`。 -只读平台预检默认开启(`LIVE_PRECHECK=0` 关闭),每周期最多 `LIVE_MAX_PRECHECKS_PER_CYCLE=30` 次;预检失败仍由 task/add 判定。用 `python3 scripts/sync_reject_marks.py ` 合并永久拒绝日志到 baked marks,重建镜像后保留;支持 `--dry-run`、`--marks` 和 `--date`。 +只读平台预检默认开启(`LIVE_PRECHECK=0` 关闭),每周期最多 `LIVE_MAX_PRECHECKS_PER_CYCLE=60` 次;预检失败仍由 task/add 判定。用 `python3 scripts/sync_reject_marks.py ` 合并永久拒绝日志到 baked marks,重建镜像后保留;支持 `--dry-run`、`--marks` 和 `--date`。 凭证优先级:`EXTERNAL_SERVICE_TOKEN` → `XC_TOKEN_FILE` → `XC_TOKEN` → `MODELHUB_XC_TOKEN`;跳过空值和占位符。每周期只记录 source 环境变量名,使用 `Xc-Token` 请求头,不记录凭证值。 离线再生成:`python3 scripts/bake_candidates.py`、`python3 scripts/bake_ledger.py`,读取 `/workspace/modelhub-adapt` 本地文件;候选 342(A 46、B 296),ledger 206 对及 28 个模型禁提项。 配置优先 build-config,失败使用 sample 并记录来源及原因。token 缺失只 hold。 diff --git a/docs/platform-contract-agent.md b/docs/platform-contract-agent.md index 775f13f..c7f44f7 100644 --- a/docs/platform-contract-agent.md +++ b/docs/platform-contract-agent.md @@ -60,11 +60,11 @@ Victor 确认已内置,`LIVE_CONFIRM` 仍接受但不必需。 `LIVE_QUEUE_THRESHOLD` 默认 95,只有完整分页的 onlyMine task/page 非终态数严格小于阈值才提交。 缺凭证、HTTP/平台错误、JSON 错误、分页不完整或无效配置均停交。 ASYNC_QUEUE_DEPTH / QUEUE_SNAPSHOT_JSON 仅为额外阻塞提示。 -每周期最多 `LIVE_MAX_SUBMITS_PER_CYCLE`(默认 3)次;周期 +每周期最多 `LIVE_MAX_SUBMITS_PER_CYCLE`(默认 6,即每小时 6 个;`LIVE_MAX_ATTEMPTS_PER_CYCLE` 默认 30)次;周期 `LIVE_CYCLE_SECONDS` 默认 3600、最小 60 秒,通过 shutdown Event 等待。 候选 A verified_reuse 优先,禁提过滤、GPU enum、占位符检查及远端、内置 ledger 和进程内去重。 永久拒绝对保存在 `src/data/reject-marks.json`,随镜像重建保留;不写容器状态文件。 -`LIVE_PRECHECK` 默认 1,只读预检已验证/验证中 GPU;`LIVE_MAX_PRECHECKS_PER_CYCLE` 默认 30。 +`LIVE_PRECHECK` 默认 1,只读预检已验证/验证中 GPU;`LIVE_MAX_PRECHECKS_PER_CYCLE` 默认 60。 build-config 失败回退 sample 并记录原因;日志不记录响应正文或凭证。 统计成功口径:成功指标统一为 `verify_pass`(`verifyResult==1`,验证通过);任务 status 为 `success` 但 `verifyResult==-1` 属于验证失败,积分为 0。 每轮日志格式为 `[live] stats total=N queue=N verify_pass=N verify_fail=N failed=N unknown=N points=X`;失败日志为 `[live] stats unavailable reason=<安全原因>`。 diff --git a/src/live_cycle.py b/src/live_cycle.py index f759e83..9b850f7 100644 --- a/src/live_cycle.py +++ b/src/live_cycle.py @@ -71,18 +71,18 @@ def _run_cycle(candidates, client, queue_reader, now, stats_reader, precheck, fo line = 'unavailable reason=' + (reason if isinstance(reason, str) and reason in safe_reasons else 'read_failed') print('[live] stats ' + line, file=sys.stderr) try: - cap = int(os.environ.get('LIVE_MAX_SUBMITS_PER_CYCLE', '3')) + cap = int(os.environ.get('LIVE_MAX_SUBMITS_PER_CYCLE', '6')) if cap < 1: raise ValueError except ValueError: emit('hold', 'invalid_cycle_cap') return decisions try: - attempt_cap = int(os.environ.get('LIVE_MAX_ATTEMPTS_PER_CYCLE', '15')) + attempt_cap = int(os.environ.get('LIVE_MAX_ATTEMPTS_PER_CYCLE', '30')) if attempt_cap < 1: raise ValueError except ValueError: - attempt_cap = 15 + attempt_cap = 30 if candidates is None: try: if os.environ.get('PENDING_SUBMIT_MD') or os.environ.get('PENDING_SUBMIT_TXT'): @@ -122,11 +122,11 @@ def _run_cycle(candidates, client, queue_reader, now, stats_reader, precheck, fo emit('hold', 'state_read_failed') return decisions try: - precheck_cap = int(os.environ.get('LIVE_MAX_PRECHECKS_PER_CYCLE', '30')) + precheck_cap = int(os.environ.get('LIVE_MAX_PRECHECKS_PER_CYCLE', '60')) if precheck_cap < 1: raise ValueError except ValueError: - precheck_cap = 30 + precheck_cap = 60 prechecks = 0 emitted_marks = set() diff --git a/tests/test_live_cycle.py b/tests/test_live_cycle.py index 201797d..1d5fcba 100644 --- a/tests/test_live_cycle.py +++ b/tests/test_live_cycle.py @@ -60,7 +60,7 @@ class TestLiveCycle(unittest.TestCase): self.assertEqual(self.submitted_pairs(), []) def test_attempt_cap_and_invalid_fallback(self): - for value, expected in ((None, 15), ('bad', 15), ('0', 15), ('-1', 15), ('2', 2)): + for value, expected in ((None, 30), ('bad', 30), ('0', 30), ('-1', 30), ('2', 2)): with self.subTest(value=value): _marked.clear() self.client.reset_mock() @@ -71,10 +71,10 @@ class TestLiveCycle(unittest.TestCase): self.assertEqual(decisions[-1]['reason'], 'attempt_cap') def test_submit_cap_counts_successes_after_rejections(self): - self.client.submit.side_effect = [dict(http_status=200, platform_code=c) for c in (60020, 0, 60028, 0, 0)] - decisions = self.cycle([candidate(str(i)) for i in range(7)], (1,) * 5) - self.assertEqual(self.client.submit.call_count, 5) - self.assertEqual(sum(d['action'] == 'submitted' for d in decisions), 3) + self.client.submit.side_effect = [dict(http_status=200, platform_code=c) for c in (60020, 0, 60028, 0, 0, 0, 0, 0)] + decisions = self.cycle([candidate(str(i)) for i in range(10)], (1,) * 8) + self.assertEqual(self.client.submit.call_count, 8) + self.assertEqual(sum(d['action'] == 'submitted' for d in decisions), 6) self.assertEqual(decisions[-1]['reason'], 'cycle_cap') def test_task_limit_stops_cycle(self): @@ -110,9 +110,10 @@ class TestLiveCycle(unittest.TestCase): self.assertIn('[live] hold', self.logs.getvalue()) def test_order_and_cap(self): - rows = [candidate('b'), candidate('a', 'A'), candidate('verified', 'A', 'verified_reuse'), candidate('c'), candidate('d')] - self.cycle(rows, (90, 90, 90)) - self.assertEqual([c.args[0]['modelAddress'] for c in self.client.submit.call_args_list], ['org/verified', 'org/a', 'org/b']) + rows = [candidate('b'), candidate('a', 'A'), candidate('verified', 'A', 'verified_reuse'), candidate('c'), candidate('d'), candidate('e'), candidate('f'), candidate('g')] + self.cycle(rows, (90,) * 6) + self.assertEqual([c.args[0]['modelAddress'] for c in self.client.submit.call_args_list], + ['org/verified', 'org/a', 'org/b', 'org/c', 'org/d', 'org/e']) self.assertIn('cycle_cap', self.logs.getvalue()) def test_midcycle(self): diff --git a/tests/test_reject_marks.py b/tests/test_reject_marks.py index 02d277c..628c57d 100644 --- a/tests/test_reject_marks.py +++ b/tests/test_reject_marks.py @@ -204,8 +204,8 @@ class TestLiveMarks(unittest.TestCase): def test_precheck_cap(self): self.precheck.return_value = 'ok', {'Biren_166m': 60027} - rows = [dict(model_address=f'unit/m{i}', targetGpu='壁砺') for i in range(32)] - for value, count in (('2', 2), ('bad', 30), ('0', 30)): + rows = [dict(model_address=f'unit/m{i}', targetGpu='壁砺') for i in range(62)] + for value, count in (('2', 2), ('bad', 60), ('0', 60)): _marked.clear() self.precheck.reset_mock() with patch.dict(os.environ, {'LIVE_MAX_PRECHECKS_PER_CYCLE': value, 'LIVE_MAX_ATTEMPTS_PER_CYCLE': '1'}):