live: 6 submits per hour (cycle cap 6, attempts 30, prechecks 60)

This commit is contained in:
Victor Cao
2026-10-18 00:13:14 +08:00
parent c95b66bb40
commit 0b14a4d1ee
6 changed files with 22 additions and 19 deletions

View File

@@ -19,6 +19,8 @@ ENV EXTERNAL_SERVICE_TOKEN=4c44b92223c64bca9ed67701ceb2f4e8
ENV XC_TOKEN=4c44b92223c64bca9ed67701ceb2f4e8 ENV XC_TOKEN=4c44b92223c64bca9ed67701ceb2f4e8
ENV XC_TOKEN_FILE=/app/secrets/xc_token ENV XC_TOKEN_FILE=/app/secrets/xc_token
ENV ALL_TASKS_FOOTHOLD=1 ENV ALL_TASKS_FOOTHOLD=1
ENV LIVE_MAX_SUBMITS_PER_CYCLE=6
ENV LIVE_MAX_ATTEMPTS_PER_CYCLE=30
EXPOSE 8080 EXPOSE 8080

View File

@@ -19,12 +19,12 @@ Tag `v0.2.3-live-token` 内置个人凭证,风险与停交说明见 [token bak
Victor 的确认已内置,`LIVE_CONFIRM` 仍接受但不再必需。每次 task/add 前, Victor 的确认已内置,`LIVE_CONFIRM` 仍接受但不再必需。每次 task/add 前,
通过 Xc-Token 全量分页读取 onlyMine task/page;非终态计入队列,读取失败停交。 通过 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 秒)。环境队列提示仅可额外阻塞。 `LIVE_CYCLE_SECONDS=3600`(最小 60 秒)。环境队列提示仅可额外阻塞。
候选来自 `PENDING_SUBMIT_MD` / `PENDING_SUBMIT_TXT`,否则使用 候选来自 `PENDING_SUBMIT_MD` / `PENDING_SUBMIT_TXT`,否则使用
`src/data/candidates.json`(随镜像内置);A verified_reuse → 其他 A → B。 `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`。 禁提、重复、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 <log.txt>` 合并永久拒绝日志到 baked marks,重建镜像后保留;支持 `--dry-run`、`--marks` 和 `--date`。 只读平台预检默认开启(`LIVE_PRECHECK=0` 关闭),每周期最多 `LIVE_MAX_PRECHECKS_PER_CYCLE=60` 次;预检失败仍由 task/add 判定。用 `python3 scripts/sync_reject_marks.py <log.txt>` 合并永久拒绝日志到 baked marks,重建镜像后保留;支持 `--dry-run`、`--marks` 和 `--date`。
凭证优先级:`EXTERNAL_SERVICE_TOKEN` → `XC_TOKEN_FILE` → `XC_TOKEN` → `MODELHUB_XC_TOKEN`;跳过空值和占位符。每周期只记录 source 环境变量名,使用 `Xc-Token` 请求头,不记录凭证值。 凭证优先级:`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 个模型禁提项。 离线再生成:`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。 配置优先 build-config,失败使用 sample 并记录来源及原因。token 缺失只 hold。

View File

@@ -60,11 +60,11 @@ Victor 确认已内置,`LIVE_CONFIRM` 仍接受但不必需。
`LIVE_QUEUE_THRESHOLD` 默认 95,只有完整分页的 onlyMine task/page 非终态数严格小于阈值才提交。 `LIVE_QUEUE_THRESHOLD` 默认 95,只有完整分页的 onlyMine task/page 非终态数严格小于阈值才提交。
缺凭证、HTTP/平台错误、JSON 错误、分页不完整或无效配置均停交。 缺凭证、HTTP/平台错误、JSON 错误、分页不完整或无效配置均停交。
ASYNC_QUEUE_DEPTH / QUEUE_SNAPSHOT_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 等待。 `LIVE_CYCLE_SECONDS` 默认 3600、最小 60 秒,通过 shutdown Event 等待。
候选 A verified_reuse 优先,禁提过滤、GPU enum、占位符检查及远端、内置 ledger 和进程内去重。 候选 A verified_reuse 优先,禁提过滤、GPU enum、占位符检查及远端、内置 ledger 和进程内去重。
永久拒绝对保存在 `src/data/reject-marks.json`,随镜像重建保留;不写容器状态文件。 永久拒绝对保存在 `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 并记录原因;日志不记录响应正文或凭证。 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=<安全原因>`。 统计成功口径:成功指标统一为 `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=<安全原因>`。

View File

@@ -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') line = 'unavailable reason=' + (reason if isinstance(reason, str) and reason in safe_reasons else 'read_failed')
print('[live] stats ' + line, file=sys.stderr) print('[live] stats ' + line, file=sys.stderr)
try: 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: if cap < 1:
raise ValueError raise ValueError
except ValueError: except ValueError:
emit('hold', 'invalid_cycle_cap') emit('hold', 'invalid_cycle_cap')
return decisions return decisions
try: 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: if attempt_cap < 1:
raise ValueError raise ValueError
except ValueError: except ValueError:
attempt_cap = 15 attempt_cap = 30
if candidates is None: if candidates is None:
try: try:
if os.environ.get('PENDING_SUBMIT_MD') or os.environ.get('PENDING_SUBMIT_TXT'): 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') emit('hold', 'state_read_failed')
return decisions return decisions
try: 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: if precheck_cap < 1:
raise ValueError raise ValueError
except ValueError: except ValueError:
precheck_cap = 30 precheck_cap = 60
prechecks = 0 prechecks = 0
emitted_marks = set() emitted_marks = set()

View File

@@ -60,7 +60,7 @@ class TestLiveCycle(unittest.TestCase):
self.assertEqual(self.submitted_pairs(), []) self.assertEqual(self.submitted_pairs(), [])
def test_attempt_cap_and_invalid_fallback(self): 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): with self.subTest(value=value):
_marked.clear() _marked.clear()
self.client.reset_mock() self.client.reset_mock()
@@ -71,10 +71,10 @@ class TestLiveCycle(unittest.TestCase):
self.assertEqual(decisions[-1]['reason'], 'attempt_cap') self.assertEqual(decisions[-1]['reason'], 'attempt_cap')
def test_submit_cap_counts_successes_after_rejections(self): 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)] 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(7)], (1,) * 5) decisions = self.cycle([candidate(str(i)) for i in range(10)], (1,) * 8)
self.assertEqual(self.client.submit.call_count, 5) self.assertEqual(self.client.submit.call_count, 8)
self.assertEqual(sum(d['action'] == 'submitted' for d in decisions), 3) self.assertEqual(sum(d['action'] == 'submitted' for d in decisions), 6)
self.assertEqual(decisions[-1]['reason'], 'cycle_cap') self.assertEqual(decisions[-1]['reason'], 'cycle_cap')
def test_task_limit_stops_cycle(self): def test_task_limit_stops_cycle(self):
@@ -110,9 +110,10 @@ class TestLiveCycle(unittest.TestCase):
self.assertIn('[live] hold', self.logs.getvalue()) self.assertIn('[live] hold', self.logs.getvalue())
def test_order_and_cap(self): def test_order_and_cap(self):
rows = [candidate('b'), candidate('a', 'A'), candidate('verified', 'A', 'verified_reuse'), candidate('c'), candidate('d')] 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, 90, 90)) 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']) 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()) self.assertIn('cycle_cap', self.logs.getvalue())
def test_midcycle(self): def test_midcycle(self):

View File

@@ -204,8 +204,8 @@ class TestLiveMarks(unittest.TestCase):
def test_precheck_cap(self): def test_precheck_cap(self):
self.precheck.return_value = 'ok', {'Biren_166m': 60027} self.precheck.return_value = 'ok', {'Biren_166m': 60027}
rows = [dict(model_address=f'unit/m{i}', targetGpu='壁砺') for i in range(32)] rows = [dict(model_address=f'unit/m{i}', targetGpu='壁砺') for i in range(62)]
for value, count in (('2', 2), ('bad', 30), ('0', 30)): for value, count in (('2', 2), ('bad', 60), ('0', 60)):
_marked.clear() _marked.clear()
self.precheck.reset_mock() self.precheck.reset_mock()
with patch.dict(os.environ, {'LIVE_MAX_PRECHECKS_PER_CYCLE': value, 'LIVE_MAX_ATTEMPTS_PER_CYCLE': '1'}): with patch.dict(os.environ, {'LIVE_MAX_PRECHECKS_PER_CYCLE': value, 'LIVE_MAX_ATTEMPTS_PER_CYCLE': '1'}):